From 5648e55774c9b62b717b376cdf6874ce11bffc67 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Fri, 23 May 2025 14:45:37 -0500 Subject: [PATCH 01/40] Add column, columnbreak, and pagebreak compatibuility to Legacy --- client/homebrew/brewRenderer/brewRenderer.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index c391d8c43..9c5e03032 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -20,6 +20,8 @@ import HeaderNav from './headerNav/headerNav.jsx'; import { safeHTML } from './safeHTML.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; +const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m; +const COLUMNBREAK_LEGACY = /\\column(:?break)?/m; const PAGE_HEIGHT = 1056; const INITIAL_CONTENT = dedent` @@ -128,7 +130,7 @@ const BrewRenderer = (props)=>{ const pagesRef = useRef(null); if(props.renderer == 'legacy') { - rawPages = props.text.split('\\page'); + rawPages = props.text.replace(COLUMNBREAK_LEGACY, '```\n````\n').split(PAGEBREAK_REGEX_LEGACY); } else { rawPages = props.text.split(PAGEBREAK_REGEX_V3); } From 77dcc9b433d020d8f96dedaf2dd84704a132099d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 28 May 2025 08:34:52 +0200 Subject: [PATCH 02/40] initial commit --- shared/naturalcrit/markdown.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index ac6988734..54884c33e 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -185,7 +185,7 @@ const mustacheSpans = { start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token - const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; + const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const match = completeSpan.exec(src); if(match) { //Find closing delimiter @@ -242,7 +242,7 @@ const mustacheDivs = { start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token - const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; + const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const match = completeBlock.exec(src); if(match) { //Find closing delimiter @@ -297,7 +297,7 @@ const mustacheInjectInline = { level : 'inline', start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; @@ -343,7 +343,7 @@ const mustacheInjectBlock = { level : 'block', start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; From 6d0d6f08b538e3cbca2abb1eca98a5b3c7abf530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 28 May 2025 09:09:14 +0200 Subject: [PATCH 03/40] initial commit --- client/homebrew/homebrew.jsx | 117 ++++++++++++++++------------------- 1 file changed, 53 insertions(+), 64 deletions(-) diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index 415390498..3559c7521 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -1,12 +1,8 @@ -//╔===--------------- Polyfills --------------===╗// +/* eslint-disable camelcase */ import 'core-js/es/string/to-well-formed.js'; -//╚===--------------- ---------------===╝// - -require('./homebrew.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const { StaticRouter:Router } = require('react-router'); -const { Route, Routes, useParams, useSearchParams } = require('react-router'); +import './homebrew.less'; +import React from 'react'; +import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; const HomePage = require('./pages/homePage/homePage.jsx'); const EditPage = require('./pages/editPage/editPage.jsx'); @@ -34,62 +30,55 @@ const WithRoute = (props)=>{ return ; }; -const Homebrew = createClass({ - displayName : 'Homebrewery', - getDefaultProps : function() { - return { - url : '', - welcomeText : '', - changelog : '', - version : '0.0.0', - account : null, - enable_v3 : false, - brew : { - title : '', - text : '', - shareId : null, - editId : null, - createdAt : null, - updatedAt : null, - lang : '' - } - }; - }, +const Homebrew = (props)=>{ + const { + url = '', + version = '0.0.0', + account = null, + enable_v3 = false, + enable_themes, + config, + brew = { + title : '', + text : '', + shareId : null, + editId : null, + createdAt : null, + updatedAt : null, + lang : '' + }, + userThemes, + brews + } = props; - getInitialState : function() { - global.account = this.props.account; - global.version = this.props.version; - global.enable_v3 = this.props.enable_v3; - global.enable_themes = this.props.enable_themes; - global.config = this.props.config; + global.account = account; + global.version = version; + global.enable_v3 = enable_v3; + global.enable_themes = enable_themes; + global.config = config; - return {}; - }, + return ( + +
+ + } /> + } /> + } /> + } /> + } /> + }/> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ ); +}; - render : function (){ - return ( - -
- - } /> - } /> - } /> - } /> - } /> - }/> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - -
-
- ); - } -}); - -module.exports = Homebrew; \ No newline at end of file +module.exports = Homebrew; From 100832195783e88e02c46c7eda0263a9c3286430 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Mon, 30 Jun 2025 12:17:46 -0500 Subject: [PATCH 04/40] Add brew snippets to local save history solves #3113 --- client/homebrew/utils/versionHistory.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/homebrew/utils/versionHistory.js b/client/homebrew/utils/versionHistory.js index ec3bd74e1..f3d6aa97b 100644 --- a/client/homebrew/utils/versionHistory.js +++ b/client/homebrew/utils/versionHistory.js @@ -42,6 +42,7 @@ function parseBrewForStorage(brew, slot = 0) { title : brew.title, text : brew.text, style : brew.style, + snippets : brew.snippets, version : brew.version, shareId : brew.shareId, savedAt : brew?.savedAt || new Date(), From 99b90e0998af579b686080571519368da2bd31af Mon Sep 17 00:00:00 2001 From: David Bolack Date: Mon, 7 Jul 2025 13:54:29 -0500 Subject: [PATCH 05/40] Include snippets in the restoration. --- client/homebrew/pages/editPage/editPage.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index f2b1e809f..d28d8ef61 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -188,8 +188,9 @@ const EditPage = createClass({ this.setState((prevState)=>({ brew : { ...prevState.brew, - style : newData.style, - text : newData.text + style : newData.style, + text : newData.text, + snippets : newData.snippets } })); }, From aa15bdaacbb762bbb0d9bd10d24cef147b5dd029 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Thu, 10 Jul 2025 19:59:09 +1200 Subject: [PATCH 06/40] Initial pass at ID validations --- .../pages/errorPage/errors/errorIndex.js | 20 +++++++++++++++++++ server/homebrew.api.js | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/client/homebrew/pages/errorPage/errors/errorIndex.js b/client/homebrew/pages/errorPage/errors/errorIndex.js index b13b19eb1..c0220b648 100644 --- a/client/homebrew/pages/errorPage/errors/errorIndex.js +++ b/client/homebrew/pages/errorPage/errors/errorIndex.js @@ -176,6 +176,26 @@ const errorIndex = (props)=>{ If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`, + // ID validation error + '11' : dedent` + ## No Homebrewery document could be found. + + The server could not locate the Homebrewery document. The Brew ID failed the validation check. + + : + + **Brew ID:** ${props.brew.brewId}`, + + // Google ID validation error + '12' : dedent` + ## No Google document could be found. + + The server could not locate the Google document. The Google ID failed the validation check. + + : + + **Brew ID:** ${props.brew.brewId}`, + //account page when account is not defined '50' : dedent` ## You are not signed in diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 84f639a4d..fc62ac763 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -48,6 +48,15 @@ const api = { } id = id.slice(googleId.length); } + + // ID Validation Checks + if(!id.match(/^[A-Za-z0-9_-]{12}$/)){ + throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id }; + } + if(googleId && !googleId.match(/^1(?:[A-Za-z0-9+\/]{32}|[A-Za-z0-9+\/]{43})$/)){ + throw { name: 'Google ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '12', brewId: id }; + } + return { id, googleId }; }, //Get array of any of this user's brews tagged with `meta:theme` From 25f25da4995cced81df49eee5f0974480dbb15c4 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Thu, 10 Jul 2025 20:39:12 +1200 Subject: [PATCH 07/40] Adjust validation regex for IDs --- server/homebrew.api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index fc62ac763..0a62ac298 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -50,7 +50,7 @@ const api = { } // ID Validation Checks - if(!id.match(/^[A-Za-z0-9_-]{12}$/)){ + if(!id.match(/^[A-Za-z0-9_-]{7,12}$/)){ throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id }; } if(googleId && !googleId.match(/^1(?:[A-Za-z0-9+\/]{32}|[A-Za-z0-9+\/]{43})$/)){ From 1794e96d50f4644417a60170552ace6a3122bf80 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Thu, 10 Jul 2025 20:46:01 +1200 Subject: [PATCH 08/40] Update tests --- server/homebrew.api.spec.js | 41 +++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js index 8bb3a0c0b..1de46b455 100644 --- a/server/homebrew.api.spec.js +++ b/server/homebrew.api.spec.js @@ -99,18 +99,51 @@ describe('Tests for api', ()=>{ expect(googleId).toBeUndefined(); }); + it('should throw if id is too short', ()=>{ + let err; + try { + api.getId({ + params : { + id : 'abcd' + } + }); + } catch (e) { + err = e; + }; + + expect(err).toEqual({ HBErrorCode: '11', brewId: 'abcd', message: 'Invalid ID', name: 'ID Error', status: 404 }); + }); + it('should return id and google id from request body', ()=>{ const { id, googleId } = api.getId({ params : { - id : 'abcdefgh' + id : 'abcdefghijkl' }, body : { - googleId : '12345' + googleId : '123456789012345678901234567890123' } }); - expect(id).toEqual('abcdefgh'); - expect(googleId).toEqual('12345'); + expect(id).toEqual('abcdefghijkl'); + expect(googleId).toEqual('123456789012345678901234567890123'); + }); + + it('should throw invalid google id', ()=>{ + let err; + try { + api.getId({ + params : { + id : 'abcdefghijkl' + }, + body : { + googleId : '012345678901234567890123456789012' + } + }); + } catch (e) { + err = e; + } + + expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 }); }); it('should return 12-char id and google id from params', ()=>{ From abef25063140d9eb3f53d2f3dbf696357b789532 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Thu, 10 Jul 2025 20:58:46 +1200 Subject: [PATCH 09/40] Update ID validation check --- server/homebrew.api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 0a62ac298..842f8a9e6 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -50,7 +50,7 @@ const api = { } // ID Validation Checks - if(!id.match(/^[A-Za-z0-9_-]{7,12}$/)){ + if(!id.match(/^[A-Za-z0-9_-]{7,14}$/)){ throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id }; } if(googleId && !googleId.match(/^1(?:[A-Za-z0-9+\/]{32}|[A-Za-z0-9+\/]{43})$/)){ From bc82afa5b2f483cb4c5ad1ec725736f615eb394d Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Thu, 10 Jul 2025 21:42:51 +1200 Subject: [PATCH 10/40] Split version from hash checks --- server/homebrew.api.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 84f639a4d..009ab408a 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -339,17 +339,22 @@ const api = { // Initialize brew from request and body, destructure query params, and set the initial value for the after-save method const brewFromClient = api.excludePropsFromUpdate(req.body); const brewFromServer = req.brew; + + if(brewFromServer?.version !== brewFromClient?.version){ + console.log(`Version mismatch on brew ${brewFromClient.editId}`); + + res.setHeader('Content-Type', 'application/json'); + return res.status(409).send(JSON.stringify({ message: `The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` })); + } + splitTextStyleAndMetadata(brewFromServer); - + brewFromServer.text = brewFromServer.text.normalize(); brewFromServer.hash = await md5(brewFromServer.text); - if((brewFromServer?.version !== brewFromClient?.version) || (brewFromServer?.hash !== brewFromClient?.hash)) { - if(brewFromClient?.version !== brewFromClient?.version) - console.log(`Version mismatch on brew ${brewFromClient.editId}`); - if(brewFromServer?.hash !== brewFromClient?.hash) { - console.log(`Hash mismatch on brew ${brewFromClient.editId}`); - } + if(brewFromServer?.hash !== brewFromClient?.hash) { + console.log(`Hash mismatch on brew ${brewFromClient.editId}`); + res.setHeader('Content-Type', 'application/json'); return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` })); } From 7f3a8185580bc460a1de0f36767ae95971caa39b Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Thu, 10 Jul 2025 23:25:57 +1200 Subject: [PATCH 11/40] Add Homebrew API coverage tests --- server/homebrew.api.spec.js | 78 +++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js index 8bb3a0c0b..e771bd8df 100644 --- a/server/homebrew.api.spec.js +++ b/server/homebrew.api.spec.js @@ -1052,4 +1052,82 @@ brew`); expect(testBrew.tags).toEqual(['tag a']); }); }); + + describe('updateBrew', ()=>{ + it('should return error on version mismatch', async ()=>{ + const brewFromClient = { version: 1 }; + const brewFromServer = { version: 1000 }; + + const req = { + brew : brewFromServer, + body : brewFromClient + }; + + await api.updateBrew(req, res); + + expect(res.status).toHaveBeenCalledWith(409); + expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}'); + }); + + it('should return error on hash mismatch', async ()=>{ + const brewFromClient = { version: 1, hash: '1234' }; + const brewFromServer = { version: 1, text: 'test' }; + + const req = { + brew : brewFromServer, + body : brewFromClient + }; + + await api.updateBrew(req, res); + + expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6'); + expect(res.status).toHaveBeenCalledWith(409); + expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}'); + }); + + it('should return error on applying patches', async ()=>{ + const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' }; + const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' }; + + const req = { + brew : brewFromServer, + body : brewFromClient + }; + + let err; + try { + await api.updateBrew(req, res); + } catch (e) { + err = e; + } + + expect(err).toEqual(Error('Invalid patch string: not a valid patch string')); + }); + + it('should save brew, no ID', async ()=>{ + const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' }; + const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' }; + + model.save = jest.fn((brew)=>{return brew;}); + + const req = { + brew : brewFromServer, + body : brewFromClient, + query : { saveToGoogle: false, removeFromGoogle: false } + }; + + await api.updateBrew(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith( + expect.objectContaining({ + _id : '1', + description : 'Test Description', + hash : '098f6bcd4621d373cade4e832627b4f6', + title : 'Test Title', + version : 2 + }) + ); + }); + }); }); From c7610cf0f8cec428147a5545f9cf56a82c8495c3 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Thu, 10 Jul 2025 07:10:13 -0500 Subject: [PATCH 12/40] Run patch processing in parallel to prior system to attempt to narrow down not-quite-so-edge cases that did not come up prior to user testing. --- client/homebrew/pages/editPage/editPage.jsx | 2 +- server/homebrew.api.js | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 7e6c03473..8eb011cab 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -265,7 +265,7 @@ const EditPage = createClass({ brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; brew.patches = stringifyPatches(makePatches(this.savedBrew.text, brew.text)); brew.hash = await md5(this.savedBrew.text); - brew.text = undefined; + //brew.text = undefined; - Temporary parallel path brew.textBin = undefined; const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 84f639a4d..e34195ffa 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -340,7 +340,7 @@ const api = { const brewFromClient = api.excludePropsFromUpdate(req.body); const brewFromServer = req.brew; splitTextStyleAndMetadata(brewFromServer); - + brewFromServer.text = brewFromServer.text.normalize(); brewFromServer.hash = await md5(brewFromServer.text); @@ -357,15 +357,20 @@ const api = { let brew = _.assign(brewFromServer, brewFromClient); brew.title = brew.title.trim(); brew.description = brew.description.trim() || ''; + try { const patches = parsePatch(brewFromClient.patches); - brew.text = applyPatches(patches, brewFromServer.text)[0]; + // Patch to a throwaway variable while parallelizing - we're more concerned with error/no error. + const patchedResult = applyPatches(patches, brewFromServer.text)[0]; + // brew.text = applyPatches(patches, brewFromServer.text)[0]; } catch (err) { console.error('Failed to apply patches:', { - patches: brewFromClient.patches, - brewId: brew.editId || 'unknown' + patches : brewFromClient.patches, + brewId : brew.editId || 'unknown', + error : err }); - throw err; // rethrow to preserve the 500 behavior + // While running in parallel, don't throw the error upstream. + // throw err; // rethrow to preserve the 500 behavior } brew.text = api.mergeBrewText(brew); From 440c7beff673e7d7e5e5379476adc535a024de2d Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 10 Jul 2025 09:47:21 -0400 Subject: [PATCH 13/40] Up to 3.19.3 so users can get the update --- changelog.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 3f85850fc..0a108b9f2 100644 --- a/changelog.md +++ b/changelog.md @@ -88,6 +88,14 @@ pre { ## changelog For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). +### Wednesday 7/09/2025 - v3.19.3 + +{{taskList +##### calculuschild +* [x] Restoring original saving behavior; will continue investigating why save was failing for some users in background +}} + + ### Wednesday 7/09/2025 - v3.19.2 {{taskList diff --git a/package-lock.json b/package-lock.json index 8ed54390e..c7f21c942 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebrewery", - "version": "3.19.2", + "version": "3.19.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebrewery", - "version": "3.19.2", + "version": "3.19.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index bc23a434e..527bfe256 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebrewery", "description": "Create authentic looking D&D homebrews using only markdown", - "version": "3.19.2", + "version": "3.19.3", "type": "module", "engines": { "npm": "^10.8.x", @@ -72,7 +72,7 @@ "lines": 50 }, "server/homebrew.api.js": { - "statements": 69, + "statements": 60, "branches": 50, "functions": 65, "lines": 70 From c5805af9358b59181f9dce686ee832164bc86748 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 10 Jul 2025 11:12:42 -0400 Subject: [PATCH 14/40] On patch failure, compare client and server text bytewise --- server/homebrew.api.js | 3 ++- shared/helpers.js | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index e34195ffa..2a7851c7f 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -11,7 +11,7 @@ import { nanoid } from 'nanoid'; import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch'; import { md5 } from 'hash-wasm'; import { splitTextStyleAndMetadata, - brewSnippetsToJSON } from '../shared/helpers.js'; + brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js'; import checkClientVersion from './middleware/check-client-version.js'; @@ -364,6 +364,7 @@ const api = { const patchedResult = applyPatches(patches, brewFromServer.text)[0]; // brew.text = applyPatches(patches, brewFromServer.text)[0]; } catch (err) { + debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); console.error('Failed to apply patches:', { patches : brewFromClient.patches, brewId : brew.editId || 'unknown', diff --git a/shared/helpers.js b/shared/helpers.js index 0ca681dfb..e09b0bdc4 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -139,9 +139,45 @@ const fetchThemeBundle = async (obj, renderer, theme)=>{ })); }; +const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { + const clientText = clientTextRaw?.normalize('NFC') || ''; + const serverText = serverTextRaw?.normalize('NFC') || ''; + + const clientBuffer = Buffer.from(clientText, 'utf8'); + const serverBuffer = Buffer.from(serverText, 'utf8'); + + if (clientBuffer.equals(serverBuffer)) { + console.log(`✅ ${label} text matches byte-for-byte.`); + return; + } + + console.warn(`❗${label} text mismatch detected.`); + console.log(`Client length: ${clientBuffer.length}`); + console.log(`Server length: ${serverBuffer.length}`); + + // Byte-level diff + for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) { + if (clientBuffer[i] !== serverBuffer[i]) { + console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`); + break; + } + } + + // Char-level diff + for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) { + if (clientText[i] !== serverText[i]) { + console.log(`Char mismatch at index ${i}:`); + console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`); + console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`); + break; + } + } +} + export { splitTextStyleAndMetadata, printCurrentBrew, fetchThemeBundle, - brewSnippetsToJSON + brewSnippetsToJSON, + debugTextMismatch }; From 45689d119e62ab9d11a81a84146e0f236dc6d728 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 10 Jul 2025 11:18:39 -0400 Subject: [PATCH 15/40] Comment out one failing test Patch failure test is no longer being thrown while we monitor in the background --- server/homebrew.api.spec.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js index e771bd8df..cb953f7e5 100644 --- a/server/homebrew.api.spec.js +++ b/server/homebrew.api.spec.js @@ -1085,24 +1085,25 @@ brew`); expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}'); }); - it('should return error on applying patches', async ()=>{ - const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' }; - const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' }; + // Commenting this one out for now, since we are no longer throwing this error while we monitor + // it('should return error on applying patches', async ()=>{ + // const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' }; + // const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' }; - const req = { - brew : brewFromServer, - body : brewFromClient - }; + // const req = { + // brew : brewFromServer, + // body : brewFromClient, + // }; - let err; - try { - await api.updateBrew(req, res); - } catch (e) { - err = e; - } + // let err; + // try { + // await api.updateBrew(req, res); + // } catch (e) { + // err = e; + // } - expect(err).toEqual(Error('Invalid patch string: not a valid patch string')); - }); + // expect(err).toEqual(Error('Invalid patch string: not a valid patch string')); + // }); it('should save brew, no ID', async ()=>{ const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' }; From 489b4b269492c3d11a5e9960b371b105c5b3f8ef Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 10 Jul 2025 12:04:09 -0400 Subject: [PATCH 16/40] Also log differences on MD5 mismatch --- server/homebrew.api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index e69a7284a..2c5244a60 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -354,7 +354,7 @@ const api = { if(brewFromServer?.hash !== brewFromClient?.hash) { console.log(`Hash mismatch on brew ${brewFromClient.editId}`); - + debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); res.setHeader('Content-Type', 'application/json'); return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` })); } From 7cadbfbd7bbd214e214d6b763b7ec753dfe34a91 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 10 Jul 2025 17:11:31 -0400 Subject: [PATCH 17/40] allowExceedingIndices for our patch applier Test if it allows patches to go through, and log error if it doesn't match the expected output. --- client/homebrew/pages/editPage/editPage.jsx | 15 ++++++++------- server/homebrew.api.js | 18 +++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 8eb011cab..48d4a0b13 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -247,6 +247,9 @@ const EditPage = createClass({ save : async function(){ if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel(); + const brewState = this.state.brew; // freeze the current state + const preSaveSnapshot = { ...brewState }; + this.setState((prevState)=>({ isSaving : true, error : null, @@ -256,12 +259,10 @@ const EditPage = createClass({ await updateHistory(this.state.brew).catch(console.error); await versionHistoryGarbageCollection().catch(console.error); - const preSaveSnapshot = { ...this.state.brew }; - //Prepare content to send to server - const brew = { ...this.state.brew }; - brew.text = brew.text.normalize(); - this.savedBrew.text = this.savedBrew.text.normalize(); + const brew = { ...brewState }; + brew.text = brew.text.normalize('NFC'); + this.savedBrew.text = this.savedBrew.text.normalize('NFC'); brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; brew.patches = stringifyPatches(makePatches(this.savedBrew.text, brew.text)); brew.hash = await md5(this.savedBrew.text); @@ -295,8 +296,8 @@ const EditPage = createClass({ shareId : res.body.shareId, version : res.body.version }, - isSaving : false, - unsavedTime : new Date() + isSaving : false, + unsavedTime : new Date() }), ()=>{ this.setState({ unsavedChanges : this.hasChanges() }); }); diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 2c5244a60..e5d622fe1 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -339,6 +339,7 @@ const api = { // Initialize brew from request and body, destructure query params, and set the initial value for the after-save method const brewFromClient = api.excludePropsFromUpdate(req.body); const brewFromServer = req.brew; + splitTextStyleAndMetadata(brewFromServer); if(brewFromServer?.version !== brewFromClient?.version){ console.log(`Version mismatch on brew ${brewFromClient.editId}`); @@ -347,9 +348,7 @@ const api = { return res.status(409).send(JSON.stringify({ message: `The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` })); } - splitTextStyleAndMetadata(brewFromServer); - - brewFromServer.text = brewFromServer.text.normalize(); + brewFromServer.text = brewFromServer.text.normalize('NFC'); brewFromServer.hash = await md5(brewFromServer.text); if(brewFromServer?.hash !== brewFromClient?.hash) { @@ -359,26 +358,27 @@ const api = { return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` })); } - let brew = _.assign(brewFromServer, brewFromClient); - brew.title = brew.title.trim(); - brew.description = brew.description.trim() || ''; - try { const patches = parsePatch(brewFromClient.patches); // Patch to a throwaway variable while parallelizing - we're more concerned with error/no error. - const patchedResult = applyPatches(patches, brewFromServer.text)[0]; + const patchedResult = applyPatches(patches, brewFromServer.text, { allowExceedingIndices: true })[0]; + if(patchedResult != brewFromClient.text) + throw("Patches did not apply cleanly, text mismatch detected"); // brew.text = applyPatches(patches, brewFromServer.text)[0]; } catch (err) { debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); console.error('Failed to apply patches:', { patches : brewFromClient.patches, - brewId : brew.editId || 'unknown', + brewId : brewFromClient.editId || 'unknown', error : err }); // While running in parallel, don't throw the error upstream. // throw err; // rethrow to preserve the 500 behavior } + let brew = _.assign(brewFromServer, brewFromClient); + brew.title = brew.title.trim(); + brew.description = brew.description.trim() || ''; brew.text = api.mergeBrewText(brew); const googleId = brew.googleId; From 9da8a1705362742ac441dd1a1e01287c61712e43 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 10 Jul 2025 17:17:25 -0400 Subject: [PATCH 18/40] Remove text mismatch logs --- package-lock.json | 7 +++++++ package.json | 1 + server/homebrew.api.js | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7f21c942..8fa722ac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@babel/preset-env": "^7.28.0", "@babel/preset-react": "^7.27.1", "@babel/runtime": "^7.27.6", + "@dmsnell/diff-match-patch": "^1.1.0", "@googleapis/drive": "^13.0.1", "@sanity/diff-match-patch": "^3.2.0", "body-parser": "^2.2.0", @@ -1888,6 +1889,12 @@ "@csstools/css-tokenizer": "^3.0.1" } }, + "node_modules/@dmsnell/diff-match-patch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@dmsnell/diff-match-patch/-/diff-match-patch-1.1.0.tgz", + "integrity": "sha512-yejLPmM5pjsGvxS9gXablUSbInW7H976c/FJ4iQxWIm7/38xBySRemTPDe34lhg1gVLbJntX0+sH0jYfU+PN9A==", + "license": "Apache-2.0" + }, "node_modules/@dual-bundle/import-meta-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", diff --git a/package.json b/package.json index 527bfe256..71e892058 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@babel/preset-env": "^7.28.0", "@babel/preset-react": "^7.27.1", "@babel/runtime": "^7.27.6", + "@dmsnell/diff-match-patch": "^1.1.0", "@googleapis/drive": "^13.0.1", "@sanity/diff-match-patch": "^3.2.0", "body-parser": "^2.2.0", diff --git a/server/homebrew.api.js b/server/homebrew.api.js index e5d622fe1..b39f3575f 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -353,7 +353,7 @@ const api = { if(brewFromServer?.hash !== brewFromClient?.hash) { console.log(`Hash mismatch on brew ${brewFromClient.editId}`); - debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); + //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); res.setHeader('Content-Type', 'application/json'); return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` })); } @@ -366,7 +366,7 @@ const api = { throw("Patches did not apply cleanly, text mismatch detected"); // brew.text = applyPatches(patches, brewFromServer.text)[0]; } catch (err) { - debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); + //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); console.error('Failed to apply patches:', { patches : brewFromClient.patches, brewId : brewFromClient.editId || 'unknown', From 22ef3cbebcff3bc9956387860c28b38d71ce8b72 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 11 Jul 2025 16:55:30 +0000 Subject: [PATCH 19/40] Gzip brew object when sending for save update --- client/homebrew/pages/editPage/editPage.jsx | 9 ++++++++- package-lock.json | 6 ++++++ package.json | 1 + server/homebrew.api.js | 14 +++++++++++++- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 48d4a0b13..b464e63b6 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -5,6 +5,7 @@ const _ = require('lodash'); const createClass = require('create-react-class'); import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; import { md5 } from 'hash-wasm'; +import { gzipSync, strToU8 } from 'fflate'; import request from '../../utils/request-middleware.js'; const { Meta } = require('vitreum/headtags'); @@ -260,7 +261,7 @@ const EditPage = createClass({ await versionHistoryGarbageCollection().catch(console.error); //Prepare content to send to server - const brew = { ...brewState }; + let brew = { ...brewState }; brew.text = brew.text.normalize('NFC'); this.savedBrew.text = this.savedBrew.text.normalize('NFC'); brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; @@ -269,10 +270,16 @@ const EditPage = createClass({ //brew.text = undefined; - Temporary parallel path brew.textBin = undefined; + brew = JSON.stringify(brew); + brew = strToU8(brew); + brew = gzipSync(brew); + const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`; const res = await request .put(`/api/update/${brew.editId}${params}`) + .set('Content-Encoding', 'gzip') + .set('Content-Type', 'application/json') .send(brew) .catch((err)=>{ console.log('Error Updating Local Brew'); diff --git a/package-lock.json b/package-lock.json index 8fa722ac7..c6305a529 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "express": "^5.1.0", "express-async-handler": "^1.2.0", "express-static-gzip": "3.0.0", + "fflate": "^0.8.2", "fs-extra": "11.3.0", "hash-wasm": "^4.12.0", "idb-keyval": "^6.2.2", @@ -6650,6 +6651,11 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", diff --git a/package.json b/package.json index 71e892058..cb6ff04a4 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "express": "^5.1.0", "express-async-handler": "^1.2.0", "express-static-gzip": "3.0.0", + "fflate": "^0.8.2", "fs-extra": "11.3.0", "hash-wasm": "^4.12.0", "idb-keyval": "^6.2.2", diff --git a/server/homebrew.api.js b/server/homebrew.api.js index b39f3575f..fb9c78ac8 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -24,6 +24,18 @@ const isStaticTheme = (renderer, themeName)=>{ return Themes[renderer]?.[themeName] !== undefined; }; +const uncompressBrew = (input, encoding)=> { + try { + const jsonStr = encoding === 'gzip' + ? zlib.gunzipSync(input).toString('utf-8') + : input.toString('utf-8'); + + return JSON.parse(jsonStr); + } catch (err) { + throw new Error('Failed to parse JSON: ' + err.message); + } +} + // const getTopBrews = (cb) => { // HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) { // cb(brews); @@ -337,7 +349,7 @@ const api = { }, updateBrew : async (req, res)=>{ // Initialize brew from request and body, destructure query params, and set the initial value for the after-save method - const brewFromClient = api.excludePropsFromUpdate(req.body); + const brewFromClient = api.excludePropsFromUpdate(uncompressBrew(req.body, req.headers['content-encoding'])); const brewFromServer = req.brew; splitTextStyleAndMetadata(brewFromServer); From fc475b2a7eb788867500b1b23ff5750cbb6ea380 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sun, 13 Jul 2025 00:52:06 -0400 Subject: [PATCH 20/40] Allow babel to transpile fflate --- scripts/project.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/project.json b/scripts/project.json index 340de077c..7385b674a 100644 --- a/scripts/project.json +++ b/scripts/project.json @@ -28,6 +28,7 @@ "codemirror/addon/hint/show-hint.js", "moment", "superagent", - "@sanity/diff-match-patch" + "@sanity/diff-match-patch", + "fflate" ] } From d3a9d813c99878b8af05ea67da8a4e3512f96b55 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sun, 13 Jul 2025 00:54:51 -0400 Subject: [PATCH 21/40] Log brew compression size just for testing purposes --- client/homebrew/pages/editPage/editPage.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index b464e63b6..de7be7c6d 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -270,9 +270,9 @@ const EditPage = createClass({ //brew.text = undefined; - Temporary parallel path brew.textBin = undefined; - brew = JSON.stringify(brew); - brew = strToU8(brew); - brew = gzipSync(brew); + const compressedBrew = gzipSync(strToU8(JSON.stringify(brew))); + console.log('uncompressed size:', (JSON.stringify(brew).length / 1024).toFixed(2), 'KB'); + console.log('compressed size', (compressedBrew.length / 1024).toFixed(2), 'KB'); const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`; @@ -280,7 +280,7 @@ const EditPage = createClass({ .put(`/api/update/${brew.editId}${params}`) .set('Content-Encoding', 'gzip') .set('Content-Type', 'application/json') - .send(brew) + .send(compressedBrew) .catch((err)=>{ console.log('Error Updating Local Brew'); this.setState({ error: err }); From 5edea7d0f434d719c7d017fbbfa57430362488a7 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sun, 13 Jul 2025 00:55:16 -0400 Subject: [PATCH 22/40] Turns out body-parser automatically inflates gzip. Can remove. --- server/homebrew.api.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index fb9c78ac8..b39f3575f 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -24,18 +24,6 @@ const isStaticTheme = (renderer, themeName)=>{ return Themes[renderer]?.[themeName] !== undefined; }; -const uncompressBrew = (input, encoding)=> { - try { - const jsonStr = encoding === 'gzip' - ? zlib.gunzipSync(input).toString('utf-8') - : input.toString('utf-8'); - - return JSON.parse(jsonStr); - } catch (err) { - throw new Error('Failed to parse JSON: ' + err.message); - } -} - // const getTopBrews = (cb) => { // HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) { // cb(brews); @@ -349,7 +337,7 @@ const api = { }, updateBrew : async (req, res)=>{ // Initialize brew from request and body, destructure query params, and set the initial value for the after-save method - const brewFromClient = api.excludePropsFromUpdate(uncompressBrew(req.body, req.headers['content-encoding'])); + const brewFromClient = api.excludePropsFromUpdate(req.body); const brewFromServer = req.brew; splitTextStyleAndMetadata(brewFromServer); From 677c02cfa530ab29b2828460d088ce25dab5b206 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 13 Jul 2025 22:36:53 +1200 Subject: [PATCH 23/40] Add forceSSL tests --- server/forcessl.mw.spec.js | 74 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 server/forcessl.mw.spec.js diff --git a/server/forcessl.mw.spec.js b/server/forcessl.mw.spec.js new file mode 100644 index 000000000..c22954f68 --- /dev/null +++ b/server/forcessl.mw.spec.js @@ -0,0 +1,74 @@ +import forceSSL from './forcessl.mw'; + +describe('Tests for ForceSSL middleware', ()=>{ + + it('should call next() when NODE_ENV is set to local', ()=>{ + const nodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'local'; + + const nextFn = jest.fn(); + + forceSSL(null, null, nextFn); + + process.env.NODE_ENV = nodeEnv; + + expect(nextFn).toHaveBeenCalled(); + }); + + it('should call next() when NODE_ENV is set to docker', ()=>{ + const nodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'docker'; + + const nextFn = jest.fn(); + + forceSSL(null, null, nextFn); + + process.env.NODE_ENV = nodeEnv; + + expect(nextFn).toHaveBeenCalled(); + }); + + it('should return 302 when header not HTTPS', ()=>{ + const nodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'test'; + + const req = { + header : ()=>{ return true; }, + get : ()=>{ return 'test'; }, + url : 'URL' + }; + + const res = { + redirect : jest.fn((code, url)=>{}) + }; + + const nextFn = jest.fn(); + + forceSSL(req, res, nextFn); + + process.env.NODE_ENV = nodeEnv; + + expect(res.redirect).toHaveBeenCalledWith(302, 'https://testURL'); + }); + + it('should call next() header is HTTPS and NODE_ENV not local or docker', ()=>{ + const nodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'test'; + + const req = { + header : ()=>{ return 'https'; } + }; + + const res = { + }; + + const nextFn = jest.fn(); + + forceSSL(req, res, nextFn); + + process.env.NODE_ENV = nodeEnv; + + expect(nextFn).toHaveBeenCalled(); + }); + +}); \ No newline at end of file From 40839b18e47694c5839b3cc56f86389c13b0d910 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Mon, 14 Jul 2025 00:14:58 +1200 Subject: [PATCH 24/40] Add tests for token.js --- server/token.spec.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 server/token.spec.js diff --git a/server/token.spec.js b/server/token.spec.js new file mode 100644 index 000000000..24ebb7f7c --- /dev/null +++ b/server/token.spec.js @@ -0,0 +1,27 @@ +import { expect, jest } from '@jest/globals'; +import config from './config.js'; + +import generateAccessToken from './token'; + +describe('Tests for Token', ()=>{ + it('Get token', ()=>{ + + // Mock the Config module, so we aren't grabbing actual secrets for testing + jest.mock('./config.js'); + config.get = jest.fn((param)=>{ + // The requested key name will be reflected to the output + return param; + }); + + const account = {}; + + const token = generateAccessToken(account); + + // If these tests fail, the config mock has failed + expect(account).toHaveProperty('issuer', 'authentication_token_issuer'); + expect(account).toHaveProperty('audience', 'authentication_token_audience'); + + // Because the inputs are fixed, this JWT key should be static + expect(typeof token).toBe('string'); + }); +}); \ No newline at end of file From 90ee08de42f417f41252ce43f1822e6d6a39e719 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Mon, 14 Jul 2025 11:06:35 +1200 Subject: [PATCH 25/40] Add text property to test object --- server/homebrew.api.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js index cb953f7e5..e6528bb9c 100644 --- a/server/homebrew.api.spec.js +++ b/server/homebrew.api.spec.js @@ -1056,7 +1056,7 @@ brew`); describe('updateBrew', ()=>{ it('should return error on version mismatch', async ()=>{ const brewFromClient = { version: 1 }; - const brewFromServer = { version: 1000 }; + const brewFromServer = { version: 1000, text: '' }; const req = { brew : brewFromServer, From 8432a6e367b5121fb793535b1b5f214a7ce40a5b Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sun, 13 Jul 2025 19:38:08 -0400 Subject: [PATCH 26/40] cleanup --- client/homebrew/pages/editPage/editPage.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index de7be7c6d..9ba54ed0a 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -261,7 +261,7 @@ const EditPage = createClass({ await versionHistoryGarbageCollection().catch(console.error); //Prepare content to send to server - let brew = { ...brewState }; + const brew = { ...brewState }; brew.text = brew.text.normalize('NFC'); this.savedBrew.text = this.savedBrew.text.normalize('NFC'); brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; @@ -271,8 +271,6 @@ const EditPage = createClass({ brew.textBin = undefined; const compressedBrew = gzipSync(strToU8(JSON.stringify(brew))); - console.log('uncompressed size:', (JSON.stringify(brew).length / 1024).toFixed(2), 'KB'); - console.log('compressed size', (compressedBrew.length / 1024).toFixed(2), 'KB'); const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`; From bc045ec6c959f7ba9f03a72382b10e63e4e5409c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 00:02:03 +0000 Subject: [PATCH 27/40] Bump the dev-dependencies group across 1 directory with 2 updates Bumps the dev-dependencies group with 2 updates in the / directory: [eslint](https://github.com/eslint/eslint) and [supertest](https://github.com/ladjs/supertest). Updates `eslint` from 9.30.1 to 9.31.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.30.1...v9.31.0) Updates `supertest` from 7.1.1 to 7.1.3 - [Release notes](https://github.com/ladjs/supertest/releases) - [Commits](https://github.com/ladjs/supertest/compare/v7.1.1...v7.1.3) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.31.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: supertest dependency-version: 7.1.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 49 +++++++++++++++++++++++++++++++---------------- package.json | 4 ++-- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6305a529..3d88ff26e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^3.1.3", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.30.1", + "eslint": "^9.31.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", @@ -76,7 +76,7 @@ "stylelint": "^16.21.1", "stylelint-config-recess-order": "^7.1.0", "stylelint-config-recommended": "^16.0.0", - "supertest": "^7.1.1" + "supertest": "^7.1.3" }, "engines": { "node": "^20.18.x", @@ -2058,10 +2058,11 @@ } }, "node_modules/@eslint/js": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", - "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -5960,18 +5961,19 @@ } }, "node_modules/eslint": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", - "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.1", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -6137,6 +6139,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -13302,9 +13317,9 @@ } }, "node_modules/superagent": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz", - "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz", + "integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==", "license": "MIT", "dependencies": { "component-emitter": "^1.3.0", @@ -13334,14 +13349,14 @@ } }, "node_modules/supertest": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz", - "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz", + "integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==", "dev": true, "license": "MIT", "dependencies": { "methods": "^1.1.2", - "superagent": "^10.2.1" + "superagent": "^10.2.2" }, "engines": { "node": ">=14.18.0" diff --git a/package.json b/package.json index cb6ff04a4..d46d91327 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^3.1.3", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.30.1", + "eslint": "^9.31.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", @@ -149,6 +149,6 @@ "stylelint": "^16.21.1", "stylelint-config-recess-order": "^7.1.0", "stylelint-config-recommended": "^16.0.0", - "supertest": "^7.1.1" + "supertest": "^7.1.3" } } From 41ff50fefe2d8ec47cf0e5437a2ac874e7e7d029 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Mon, 14 Jul 2025 21:23:38 +1200 Subject: [PATCH 28/40] Add missing test --- tests/html/safeHTML.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/html/safeHTML.test.js b/tests/html/safeHTML.test.js index 51fa1e995..cb5466a48 100644 --- a/tests/html/safeHTML.test.js +++ b/tests/html/safeHTML.test.js @@ -4,6 +4,17 @@ require('jsdom-global')(); import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML'; +test('Exit if no document', function() { + const doc = document; + document = undefined; + + const result = safeHTML(''); + + document = doc; + + expect(result).toBe(null); +}); + test('Javascript via href', function() { const source = `Click me`; const rendered = safeHTML(source); From 248d2038ece9dd15d2215dcda1100324cdc5573c Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 14 Jul 2025 13:10:19 -0400 Subject: [PATCH 29/40] Cleanup token.js --- server/token.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/server/token.js b/server/token.js index 7a23dff4b..feaea8d33 100644 --- a/server/token.js +++ b/server/token.js @@ -5,21 +5,16 @@ import config from './config.js'; const generateAccessToken = (account)=>{ const payload = account; - // When the token was issued - payload.issued = (new Date()); - // Which service issued the Token - payload.issuer = config.get('authentication_token_issuer'); - // Which service is the token intended for - payload.audience = config.get('authentication_token_audience'); - // The signing key for signing the token + payload.issued = (new Date()); // When the token was issued + payload.issuer = config.get('authentication_token_issuer'); // Which service issued the Token + payload.audience = config.get('authentication_token_audience'); // Which service is the token intended for + const secret = config.get('authentication_token_secret'); // The signing key for signing the token + delete payload.password; delete payload._id; - const secret = config.get('authentication_token_secret'); - const token = jwt.encode(payload, secret); - return token; }; -export default generateAccessToken; \ No newline at end of file +export default generateAccessToken; From f9e7aa355ddafc9b3d4322909030fc36509c1bba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 20:09:17 +0000 Subject: [PATCH 30/40] Bump the prod-dependencies group across 1 directory with 2 updates Bumps the prod-dependencies group with 2 updates in the / directory: [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) and [mongoose](https://github.com/Automattic/mongoose). Updates `core-js` from 3.43.0 to 3.44.0 - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/commits/v3.44.0/packages/core-js) Updates `mongoose` from 8.16.1 to 8.16.3 - [Release notes](https://github.com/Automattic/mongoose/releases) - [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md) - [Commits](https://github.com/Automattic/mongoose/compare/8.16.1...8.16.3) --- updated-dependencies: - dependency-name: core-js dependency-version: 3.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-dependencies - dependency-name: mongoose dependency-version: 8.16.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 ++++++++++-------- package.json | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d88ff26e..aafd96dda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "classnames": "^2.5.1", "codemirror": "^5.65.6", "cookie-parser": "^1.4.7", - "core-js": "^3.43.0", + "core-js": "^3.44.0", "cors": "^2.8.5", "create-react-class": "^15.7.0", "dedent-tabs": "^0.10.3", @@ -49,7 +49,7 @@ "marked-subsuper-text": "^1.0.3", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", - "mongoose": "^8.16.1", + "mongoose": "^8.16.3", "nanoid": "5.1.5", "nconf": "^0.13.0", "react": "^18.3.1", @@ -5048,10 +5048,11 @@ } }, "node_modules/core-js": { - "version": "3.43.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz", - "integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==", + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz", + "integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -10189,9 +10190,10 @@ } }, "node_modules/mongoose": { - "version": "8.16.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.1.tgz", - "integrity": "sha512-Q+0TC+KLdY4SYE+u9gk9pdW1tWu/pl0jusyEkMGTgBoAbvwQdfy4f9IM8dmvCwb/blSfp7IfLkob7v76x6ZGpQ==", + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.3.tgz", + "integrity": "sha512-p2JOsRQG7j0vXhLpsWw5Slm2VnDeJK8sRyqSyegk5jQujuP9BTOZ1Di9VX/0lYfBhZ2DpAExi51QTd4pIqSgig==", + "license": "MIT", "dependencies": { "bson": "^6.10.4", "kareem": "2.6.3", diff --git a/package.json b/package.json index d46d91327..183a74082 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "classnames": "^2.5.1", "codemirror": "^5.65.6", "cookie-parser": "^1.4.7", - "core-js": "^3.43.0", + "core-js": "^3.44.0", "cors": "^2.8.5", "create-react-class": "^15.7.0", "dedent-tabs": "^0.10.3", @@ -122,7 +122,7 @@ "marked-subsuper-text": "^1.0.3", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", - "mongoose": "^8.16.1", + "mongoose": "^8.16.3", "nanoid": "5.1.5", "nconf": "^0.13.0", "react": "^18.3.1", From 973e071e930461560252e6493620780edbb31474 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 15 Jul 2025 08:13:35 +1200 Subject: [PATCH 31/40] Slightly loosen Google ID match criteria, add comments --- server/homebrew.api.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index bd8764b51..82d64c1a3 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -50,10 +50,15 @@ const api = { } // ID Validation Checks + // Homebrewery ID + // Typically 12 characters, but the DB shows a range of 7 to 14 characters if(!id.match(/^[A-Za-z0-9_-]{7,14}$/)){ throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id }; } - if(googleId && !googleId.match(/^1(?:[A-Za-z0-9+\/]{32}|[A-Za-z0-9+\/]{43})$/)){ + // Google ID + // Typically 33 characters, old format is 44 - always starts with a 1 + // Managed by Google, may change outside of our control, so any length between 33 and 44 is acceptable + if(googleId && !googleId.match(/^1(?:[A-Za-z0-9+\/]{32,43})$/)){ throw { name: 'Google ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '12', brewId: id }; } From 828208aadb9b71a90a3e545f0c27728d1f81296e Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 15 Jul 2025 08:19:05 +1200 Subject: [PATCH 32/40] Add more ID validation test cases --- server/homebrew.api.spec.js | 38 ++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js index 0b57588ea..0a6d1d452 100644 --- a/server/homebrew.api.spec.js +++ b/server/homebrew.api.spec.js @@ -128,7 +128,7 @@ describe('Tests for api', ()=>{ expect(googleId).toEqual('123456789012345678901234567890123'); }); - it('should throw invalid google id', ()=>{ + it('should throw invalid - google id right length but does not match pattern', ()=>{ let err; try { api.getId({ @@ -146,6 +146,42 @@ describe('Tests for api', ()=>{ expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 }); }); + it('should throw invalid - google id too short (32 char)', ()=>{ + let err; + try { + api.getId({ + params : { + id : 'abcdefghijkl' + }, + body : { + googleId : '12345678901234567890123456789012' + } + }); + } catch (e) { + err = e; + } + + expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 }); + }); + + it('should throw invalid - google id too long (45 char)', ()=>{ + let err; + try { + api.getId({ + params : { + id : 'abcdefghijkl' + }, + body : { + googleId : '123456789012345678901234567890123456789012345' + } + }); + } catch (e) { + err = e; + } + + expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 }); + }); + it('should return 12-char id and google id from params', ()=>{ const { id, googleId } = api.getId({ params : { From 90e577dd3fd0e45023c8a64fe05a52957e492470 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 15 Jul 2025 09:02:57 +1200 Subject: [PATCH 33/40] Rework tests --- server/forcessl.mw.spec.js | 96 +++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/server/forcessl.mw.spec.js b/server/forcessl.mw.spec.js index c22954f68..e18821e6d 100644 --- a/server/forcessl.mw.spec.js +++ b/server/forcessl.mw.spec.js @@ -1,73 +1,65 @@ import forceSSL from './forcessl.mw'; describe('Tests for ForceSSL middleware', ()=>{ + let originalEnv; + let nextFn; - it('should call next() when NODE_ENV is set to local', ()=>{ - const nodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = 'local'; + let req = {}; + let res = {}; - const nextFn = jest.fn(); + beforeEach(()=>{ + originalEnv = process.env.NODE_ENV; + nextFn = jest.fn(); - forceSSL(null, null, nextFn); - - process.env.NODE_ENV = nodeEnv; - - expect(nextFn).toHaveBeenCalled(); - }); - - it('should call next() when NODE_ENV is set to docker', ()=>{ - const nodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = 'docker'; - - const nextFn = jest.fn(); - - forceSSL(null, null, nextFn); - - process.env.NODE_ENV = nodeEnv; - - expect(nextFn).toHaveBeenCalled(); - }); - - it('should return 302 when header not HTTPS', ()=>{ - const nodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = 'test'; - - const req = { - header : ()=>{ return true; }, + req = { + header : ()=>{ return 'http'; }, get : ()=>{ return 'test'; }, url : 'URL' }; - const res = { - redirect : jest.fn((code, url)=>{}) + res = { + redirect : jest.fn() }; - - const nextFn = jest.fn(); - - forceSSL(req, res, nextFn); - - process.env.NODE_ENV = nodeEnv; - - expect(res.redirect).toHaveBeenCalledWith(302, 'https://testURL'); + }); + afterEach(()=>{ + process.env.NODE_ENV = originalEnv; + jest.clearAllMocks(); }); - it('should call next() header is HTTPS and NODE_ENV not local or docker', ()=>{ - const nodeEnv = process.env.NODE_ENV; + it('should not redirect when NODE_ENV is set to local', ()=>{ + process.env.NODE_ENV = 'local'; + + forceSSL(null, null, nextFn); + + expect(res.redirect).not.toHaveBeenCalled(); + expect(nextFn).toHaveBeenCalled(); + }); + + it('should not redirect when NODE_ENV is set to docker', ()=>{ + process.env.NODE_ENV = 'docker'; + + forceSSL(null, null, nextFn); + + expect(res.redirect).not.toHaveBeenCalled(); + expect(nextFn).toHaveBeenCalled(); + }); + + it('should redirect with 302 when header is not HTTPS and NODE_ENV is not local or docker', ()=>{ process.env.NODE_ENV = 'test'; - const req = { - header : ()=>{ return 'https'; } - }; - - const res = { - }; - - const nextFn = jest.fn(); - forceSSL(req, res, nextFn); - process.env.NODE_ENV = nodeEnv; + expect(res.redirect).toHaveBeenCalledWith(302, 'https://testURL'); + expect(nextFn).not.toHaveBeenCalled(); + }); + it('should not redirect when header is HTTPS and NODE_ENV is not local or docker', ()=>{ + process.env.NODE_ENV = 'test'; + req.header = ()=>{ return 'https'; }; + + forceSSL(req, res, nextFn); + + expect(res.redirect).not.toHaveBeenCalled(); expect(nextFn).toHaveBeenCalled(); }); From 6de7a64acdabdf150d376501cd78bbc795e793ba Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 15 Jul 2025 12:58:06 -0400 Subject: [PATCH 34/40] Add comment for to-well-formed --- client/homebrew/homebrew.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index 3559c7521..1f20b2e93 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import 'core-js/es/string/to-well-formed.js'; +import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers import './homebrew.less'; import React from 'react'; import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; From ddfa06e76bc66eb704a51f077988da7d9a09c411 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 15 Jul 2025 17:17:09 +0000 Subject: [PATCH 35/40] Change requires to imports --- client/homebrew/homebrew.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index 1f20b2e93..fb94e7661 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -4,14 +4,14 @@ import './homebrew.less'; import React from 'react'; import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; -const HomePage = require('./pages/homePage/homePage.jsx'); -const EditPage = require('./pages/editPage/editPage.jsx'); -const UserPage = require('./pages/userPage/userPage.jsx'); -const SharePage = require('./pages/sharePage/sharePage.jsx'); -const NewPage = require('./pages/newPage/newPage.jsx'); -const ErrorPage = require('./pages/errorPage/errorPage.jsx'); -const VaultPage = require('./pages/vaultPage/vaultPage.jsx'); -const AccountPage = require('./pages/accountPage/accountPage.jsx'); +import HomePage from './pages/homePage/homePage.jsx'; +import EditPage from './pages/editPage/editPage.jsx'; +import UserPage from './pages/userPage/userPage.jsx'; +import SharePage from './pages/sharePage/sharePage.jsx'; +import NewPage from './pages/newPage/newPage.jsx'; +import ErrorPage from './pages/errorPage/errorPage.jsx'; +import VaultPage from './pages/vaultPage/vaultPage.jsx'; +import AccountPage from './pages/accountPage/accountPage.jsx'; const WithRoute = (props)=>{ const params = useParams(); From 0a02f910f82428463be745df3d22c90dd9a96fda Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 15 Jul 2025 17:32:10 +0000 Subject: [PATCH 36/40] Clean up WithRoute --- client/homebrew/homebrew.jsx | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index fb94e7661..a6b4b9175 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -13,21 +13,12 @@ import ErrorPage from './pages/errorPage/errorPage.jsx'; import VaultPage from './pages/vaultPage/vaultPage.jsx'; import AccountPage from './pages/accountPage/accountPage.jsx'; -const WithRoute = (props)=>{ +const WithRoute = ({ el: Element, ...rest })=>{ const params = useParams(); const [searchParams] = useSearchParams(); - const queryParams = {}; - for (const [key, value] of searchParams?.entries() || []) { - queryParams[key] = value; - } - const Element = props.el; - const allProps = { - ...props, - ...params, - query : queryParams, - el : undefined - }; - return ; + const queryParams = Object.fromEntries(searchParams?.entries() || []); + + return ; }; const Homebrew = (props)=>{ @@ -51,11 +42,11 @@ const Homebrew = (props)=>{ brews } = props; - global.account = account; - global.version = version; - global.enable_v3 = enable_v3; + global.account = account; + global.version = version; + global.enable_v3 = enable_v3; global.enable_themes = enable_themes; - global.config = config; + global.config = config; return ( @@ -81,4 +72,4 @@ const Homebrew = (props)=>{ ); }; -module.exports = Homebrew; +module.exports = Homebrew; \ No newline at end of file From b587d1739773872d604afa4a35d9f0b3f96c2aee Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 15 Jul 2025 17:41:56 +0000 Subject: [PATCH 37/40] Remove unused React import --- client/homebrew/homebrew.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index a6b4b9175..466b14a8c 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -1,7 +1,6 @@ /* eslint-disable camelcase */ import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers import './homebrew.less'; -import React from 'react'; import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; import HomePage from './pages/homePage/homePage.jsx'; From b6c03e88b8a18a7374b880f13c1692dd0db1725f Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 15 Jul 2025 17:53:01 +0000 Subject: [PATCH 38/40] Looks like react is needed by some other components later on --- client/homebrew/homebrew.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index 466b14a8c..a6b4b9175 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -1,6 +1,7 @@ /* eslint-disable camelcase */ import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers import './homebrew.less'; +import React from 'react'; import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; import HomePage from './pages/homePage/homePage.jsx'; From fbe637ff8276e11e5a580e24d8fb617369e5d064 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 15 Jul 2025 14:16:17 -0400 Subject: [PATCH 39/40] Add to non-quoted case as well `{{greenBox,height:calc(10px*2) }}` should also be valid without using quotes. --- shared/naturalcrit/markdown.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 54884c33e..78107dcf4 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -185,7 +185,7 @@ const mustacheSpans = { start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token - const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; + const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const match = completeSpan.exec(src); if(match) { //Find closing delimiter @@ -242,7 +242,7 @@ const mustacheDivs = { start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token - const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; + const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const match = completeBlock.exec(src); if(match) { //Find closing delimiter @@ -297,7 +297,7 @@ const mustacheInjectInline = { level : 'inline', start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/g; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; @@ -343,7 +343,7 @@ const mustacheInjectBlock = { level : 'block', start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; From 3626ed5a31ebb9b0f7a6390cc7ffd6017fa793aa Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 15 Jul 2025 14:47:04 -0400 Subject: [PATCH 40/40] Rename regex, move column replacement Renaming COLUMNBREAK_REGEX_LEGACY for consistency in naming scheme with the other regexes. Moving the legacy `\column` replacement down to `renderPages()` where we do similar text modification steps for V3. --- client/homebrew/brewRenderer/brewRenderer.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index dbb10e653..6bcfc87ec 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -21,7 +21,7 @@ import { safeHTML } from './safeHTML.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m; -const COLUMNBREAK_LEGACY = /\\column(:?break)?/m; +const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m; const PAGE_HEIGHT = 1056; const INITIAL_CONTENT = dedent` @@ -132,7 +132,7 @@ const BrewRenderer = (props)=>{ const pagesRef = useRef(null); if(props.renderer == 'legacy') { - rawPages = props.text.replace(COLUMNBREAK_LEGACY, '```\n````\n').split(PAGEBREAK_REGEX_LEGACY); + rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY); } else { rawPages = props.text.split(PAGEBREAK_REGEX_V3); } @@ -189,6 +189,7 @@ const BrewRenderer = (props)=>{ let attributes = {}; if(props.renderer == 'legacy') { + pageText.replace(COLUMNBREAK_REGEX_LEGACY, '```\n````\n'); // Allow Legacy brews to use `\column(break)` const html = MarkdownLegacy.render(pageText); return ;