0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-24 03:23:02 +00:00

Compare commits

..

99 Commits

Author SHA1 Message Date
Trevor Buckner
253dbb358b Merge pull request #4366 from naturalcrit/relocateSplitPane
Moving splitPane over to the components folder
2025-07-29 16:36:39 -04:00
Trevor Buckner
719edd82c5 Moving splitPane over to the components folder
Just to reduce the number of changes needed to review on the UI overhaul #4122 PR
2025-07-29 16:35:25 -04:00
Trevor Buckner
63d957fdc6 Merge pull request #4357 from naturalcrit/dependabot/npm_and_yarn/dev-dependencies-e74ffdea55
Bump the dev-dependencies group across 1 directory with 3 updates
2025-07-23 16:39:37 -04:00
dependabot[bot]
7751c0e37b Bump the dev-dependencies group across 1 directory with 3 updates
Bumps the dev-dependencies group with 3 updates in the / directory: [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest), [stylelint](https://github.com/stylelint/stylelint) and [supertest](https://github.com/ladjs/supertest).


Updates `jest` from 30.0.4 to 30.0.5
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest)

Updates `stylelint` from 16.21.1 to 16.22.0
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/16.21.1...16.22.0)

Updates `supertest` from 7.1.3 to 7.1.4
- [Release notes](https://github.com/ladjs/supertest/releases)
- [Commits](https://github.com/ladjs/supertest/compare/v7.1.3...v7.1.4)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: stylelint
  dependency-version: 16.22.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: supertest
  dependency-version: 7.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 03:52:56 +00:00
Trevor Buckner
990bf80b59 Comment out patch contents from logs
patch contents on failed patches clogging logs with pages and pages of text
2025-07-22 14:45:57 -04:00
Trevor Buckner
f16598f238 Fix Google ID Validation Regex
Google IDs with underscores were failing.

Regex found in Google drive documentation: https://developers.google.com/workspace/docs/api/concepts/document
2025-07-22 14:39:09 -04:00
Trevor Buckner
579e9e0ec5 Merge pull request #4347 from G-Ambatte/experimentalDiffSaveFix
Diff patching fix using encodeURI
2025-07-19 15:30:10 -04:00
Trevor Buckner
f6629f2f9e Merge pull request #4287 from dbolack-ab/opengraph_locale
Add brew locale to opengraph localization
2025-07-19 15:27:44 -04:00
G.Ambatte
b87c78474d Fix for diff patching using encodeURI 2025-07-19 14:49:02 +12:00
Trevor Buckner
958d282a58 Merge branch 'master' into opengraph_locale 2025-07-17 14:30:16 -04:00
David Bolack
7e56ae2019 locale typo. 2025-07-16 10:34:16 -05:00
David Bolack
ebca50ed4b Merge branch 'master' into opengraph_locale 2025-07-16 10:33:27 -05:00
Trevor Buckner
bfd14757c2 Merge pull request #4210 from dbolack-ab/legacy_gmb
Add column, columnbreak, and pagebreak compatibulity to Legacy
2025-07-15 15:58:17 -04:00
Trevor Buckner
3626ed5a31 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.
2025-07-15 14:47:04 -04:00
Trevor Buckner
d385bacdd6 Merge branch 'master' into legacy_gmb 2025-07-15 14:22:31 -04:00
Trevor Buckner
cbbb2c0a7d Merge pull request #4225 from naturalcrit/fix-calc-in-curly-elements
Fix calc operator regex
2025-07-15 14:21:35 -04:00
Trevor Buckner
fbe637ff82 Add to non-quoted case as well
`{{greenBox,height:calc(10px*2) }}` should also be valid without using quotes.
2025-07-15 14:16:17 -04:00
Trevor Buckner
82bd16c623 Merge branch 'master' into fix-calc-in-curly-elements 2025-07-15 14:09:02 -04:00
Trevor Buckner
d1f13af67b Merge pull request #4340 from naturalcrit/MoreHomebrew.jsxCleanup
More homebrew.jsx cleanup
2025-07-15 13:56:39 -04:00
Trevor Buckner
b6c03e88b8 Looks like react is needed by some other components later on 2025-07-15 17:53:01 +00:00
Trevor Buckner
b587d17397 Remove unused React import 2025-07-15 17:41:56 +00:00
Trevor Buckner
0a02f910f8 Clean up WithRoute 2025-07-15 17:32:10 +00:00
Trevor Buckner
ddfa06e76b Change requires to imports 2025-07-15 17:17:09 +00:00
Trevor Buckner
0c2b1fec04 Merge pull request #4226 from naturalcrit/refactor-homebrew.jsx-to-functional
refactor homebrew.jsx to be functional
2025-07-15 12:59:19 -04:00
Trevor Buckner
6de7a64acd Add comment for to-well-formed 2025-07-15 12:58:06 -04:00
Trevor Buckner
b9fe4c3901 Merge branch 'master' into refactor-homebrew.jsx-to-functional 2025-07-15 11:32:28 -04:00
Trevor Buckner
5ae01862e5 Merge pull request #4339 from naturalcrit/dependabot/npm_and_yarn/prod-dependencies-b017ff4ab1
Bump the prod-dependencies group across 1 directory with 2 updates
2025-07-15 11:17:59 -04:00
Trevor Buckner
398df7a061 Merge branch 'master' into dependabot/npm_and_yarn/prod-dependencies-b017ff4ab1 2025-07-15 11:15:54 -04:00
Trevor Buckner
443b0f6a37 Merge pull request #4320 from G-Ambatte/experimentalIDValidations
Brew ID validations
2025-07-15 11:14:45 -04:00
Trevor Buckner
544175b994 Merge branch 'master' into experimentalIDValidations 2025-07-15 11:14:11 -04:00
Trevor Buckner
955602e7ee Merge pull request #4333 from G-Ambatte/addForceSSLTests
Add forceSSL middleware tests
2025-07-15 11:11:33 -04:00
G.Ambatte
90e577dd3f Rework tests 2025-07-15 09:02:57 +12:00
G.Ambatte
828208aadb Add more ID validation test cases 2025-07-15 08:19:05 +12:00
G.Ambatte
973e071e93 Slightly loosen Google ID match criteria, add comments 2025-07-15 08:13:35 +12:00
dependabot[bot]
f9e7aa355d 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] <support@github.com>
2025-07-14 20:09:17 +00:00
Trevor Buckner
24dfd41714 Merge branch 'master' into experimentalIDValidations 2025-07-14 13:37:19 -04:00
Trevor Buckner
638e54535d Merge branch 'master' into addForceSSLTests 2025-07-14 13:16:14 -04:00
Trevor Buckner
cbc6956221 Merge pull request #4334 from G-Ambatte/addTokenTests
Add token.js tests
2025-07-14 13:15:43 -04:00
Trevor Buckner
248d2038ec Cleanup token.js 2025-07-14 13:10:19 -04:00
Trevor Buckner
5b66175b8c Merge pull request #4337 from naturalcrit/dependabot/npm_and_yarn/dev-dependencies-3db4bbab60
Bump the dev-dependencies group across 1 directory with 2 updates
2025-07-14 12:59:01 -04:00
Trevor Buckner
552aa7d41a Merge branch 'master' into dependabot/npm_and_yarn/dev-dependencies-3db4bbab60 2025-07-14 12:53:33 -04:00
Trevor Buckner
b0a108b543 Merge pull request #4338 from G-Ambatte/addSafeHTMLTest
Increase safeHTML testing to 100% coverage
2025-07-14 12:53:12 -04:00
G.Ambatte
505d2840c0 Merge branch 'master' into addSafeHTMLTest 2025-07-14 21:26:22 +12:00
G.Ambatte
41ff50fefe Add missing test 2025-07-14 21:23:38 +12:00
G.Ambatte
2fbcc84a50 Merge branch 'master' into experimentalIDValidations 2025-07-14 14:50:05 +12:00
G.Ambatte
45e4d27c0a Merge branch 'master' into addForceSSLTests 2025-07-14 14:46:10 +12:00
G.Ambatte
77bf3ffc6f Merge branch 'master' into addTokenTests 2025-07-14 14:46:07 +12:00
dependabot[bot]
bc045ec6c9 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] <support@github.com>
2025-07-14 00:02:03 +00:00
Trevor Buckner
6390ea076a Merge pull request #4330 from naturalcrit/CompressSaveDataUpload
Gzip brew object when sending for save update
2025-07-13 20:00:21 -04:00
Trevor Buckner
6affcb587d Merge branch 'master' into CompressSaveDataUpload 2025-07-13 19:57:10 -04:00
Trevor Buckner
7787afabff Merge pull request #4336 from naturalcrit/fixVersionTest
Fix test option 2 - Add text property to version check test object
2025-07-13 19:55:16 -04:00
Trevor Buckner
fb4a8e5cf1 Merge branch 'CompressSaveDataUpload' of https://github.com/naturalcrit/homebrewery into CompressSaveDataUpload 2025-07-13 19:38:10 -04:00
Trevor Buckner
8432a6e367 cleanup 2025-07-13 19:38:08 -04:00
G.Ambatte
90ee08de42 Add text property to test object 2025-07-14 11:06:35 +12:00
G.Ambatte
40839b18e4 Add tests for token.js 2025-07-14 00:14:58 +12:00
G.Ambatte
677c02cfa5 Add forceSSL tests 2025-07-13 22:36:53 +12:00
G.Ambatte
a7a8803e9d Merge branch 'master' into experimentalIDValidations 2025-07-13 20:52:52 +12:00
Trevor Buckner
5fbc111db7 Merge branch 'master' into CompressSaveDataUpload 2025-07-13 00:55:59 -04:00
Trevor Buckner
5edea7d0f4 Turns out body-parser automatically inflates gzip. Can remove. 2025-07-13 00:55:16 -04:00
Trevor Buckner
d3a9d813c9 Log brew compression size just for testing purposes 2025-07-13 00:54:51 -04:00
Trevor Buckner
fc475b2a7e Allow babel to transpile fflate 2025-07-13 00:52:06 -04:00
Trevor Buckner
76b76b3bb6 Merge pull request #4286 from dbolack-ab/snippets-save-history-too
Add brew snippets to local save history
2025-07-11 13:32:13 -04:00
Trevor Buckner
22ef3cbebc Gzip brew object when sending for save update 2025-07-11 16:55:30 +00:00
Trevor Buckner
9da8a17053 Remove text mismatch logs 2025-07-10 17:17:25 -04:00
Trevor Buckner
7cadbfbd7b allowExceedingIndices for our patch applier
Test if it allows patches to go through, and log error if it doesn't match the expected output.
2025-07-10 17:11:31 -04:00
Trevor Buckner
98b9e86787 Merge pull request #4329 from naturalcrit/AdditionalPatchLogging
On patch failure, compare client and server text bytewise
2025-07-10 12:05:24 -04:00
Trevor Buckner
489b4b2694 Also log differences on MD5 mismatch 2025-07-10 12:04:09 -04:00
Trevor Buckner
8d279260c2 Merge branch 'master' into AdditionalPatchLogging 2025-07-10 11:19:07 -04:00
Trevor Buckner
7c08c430d0 Merge pull request #4324 from G-Ambatte/experimentalSplitHashAndVersionChecks
Split hash and version checks in updateBrew function
2025-07-10 11:18:53 -04:00
Trevor Buckner
45689d119e Comment out one failing test
Patch failure test is no longer being thrown while we monitor in the background
2025-07-10 11:18:39 -04:00
Trevor Buckner
c5805af935 On patch failure, compare client and server text bytewise 2025-07-10 11:12:42 -04:00
Trevor Buckner
b2c4bb7082 Merge branch 'master' into experimentalSplitHashAndVersionChecks 2025-07-10 11:06:29 -04:00
Trevor Buckner
68460447dc Merge pull request #4327 from dbolack-ab/fallbackTest
Run patch processing in parallel to prior system
2025-07-10 09:50:43 -04:00
Trevor Buckner
440c7beff6 Up to 3.19.3 so users can get the update 2025-07-10 09:47:21 -04:00
David Bolack
c7610cf0f8 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. 2025-07-10 07:10:13 -05:00
G.Ambatte
7f3a818558 Add Homebrew API coverage tests 2025-07-10 23:25:57 +12:00
G.Ambatte
bc82afa5b2 Split version from hash checks 2025-07-10 21:42:51 +12:00
G.Ambatte
abef250631 Update ID validation check 2025-07-10 20:58:46 +12:00
G.Ambatte
1794e96d50 Update tests 2025-07-10 20:46:01 +12:00
G.Ambatte
25f25da499 Adjust validation regex for IDs 2025-07-10 20:39:12 +12:00
G.Ambatte
aa15bdaacb Initial pass at ID validations 2025-07-10 19:59:09 +12:00
Trevor Buckner
7ba7991631 Additional diff server error logging 2025-07-10 00:37:03 -04:00
Trevor Buckner
0e1ac26999 v3.19.2 2025-07-09 22:42:21 -04:00
Trevor Buckner
f49fed8c35 Merge pull request #4311 from dbolack-ab/md5Fix
Normalize strings before running MD5s
2025-07-09 22:40:15 -04:00
Trevor Buckner
a8236fbab4 Ok. I'm lowering the coverage threshold 2025-07-09 22:14:24 -04:00
Trevor Buckner
daf4eceedd Small rearrangement 2025-07-09 22:09:16 -04:00
David Bolack
a02361ee65 Move normalization to before diffing 2025-07-09 19:37:57 -05:00
David Bolack
81e20f032e NOrmalize strings before rnuning MD5s 2025-07-09 18:52:45 -05:00
Trevor Buckner
1d92b98568 Merge pull request #4310 from dbolack-ab/2025-06-09_Debug
Add Patch wrapper/unwrapper for saves
2025-07-09 18:42:23 -04:00
David Bolack
0f4157d084 Add Patch wrapper/unwrapper for saves
Object encapsulation for the win?
2025-07-09 17:16:47 -05:00
Trevor Buckner
4dcc3749d8 Merge branch 'master' into snippets-save-history-too 2025-07-09 13:04:08 -04:00
Trevor Buckner
8f058d56f2 Merge pull request #4304 from naturalcrit/3.19.1
up To V3.19.1
2025-07-09 11:11:59 -04:00
David Bolack
99b90e0998 Include snippets in the restoration. 2025-07-07 13:54:29 -05:00
David Bolack
702ece6671 Add brew locale to opengraph localization 2025-06-30 12:39:30 -05:00
David Bolack
1008321957 Add brew snippets to local save history
solves #3113
2025-06-30 12:17:46 -05:00
David Bolack
b547486c48 Merge branch 'master' into legacy_gmb 2025-06-30 10:54:35 -05:00
Víctor Losada Hernández
6d0d6f08b5 initial commit 2025-05-28 09:09:14 +02:00
Víctor Losada Hernández
77dcc9b433 initial commit 2025-05-28 08:34:52 +02:00
David Bolack
5648e55774 Add column, columnbreak, and pagebreak compatibuility to Legacy 2025-05-23 14:45:37 -05:00
23 changed files with 1206 additions and 598 deletions

View File

@@ -88,6 +88,21 @@ 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
##### calculuschild
* [x] Hotfix for saving issues - Please refresh your browser and report if problems continue
}}
### Wednesday 7/09/2025 - v3.19.1
{{taskList

View File

@@ -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_REGEX_LEGACY = /\\column(:?break)?/m;
const PAGE_HEIGHT = 1056;
const INITIAL_CONTENT = dedent`
@@ -130,7 +132,7 @@ const BrewRenderer = (props)=>{
const pagesRef = useRef(null);
if(props.renderer == 'legacy') {
rawPages = props.text.split('\\page');
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
} else {
rawPages = props.text.split(PAGEBREAK_REGEX_V3);
}
@@ -187,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 <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;

View File

@@ -1,95 +1,75 @@
//╔===--------------- Polyfills --------------===╗//
import 'core-js/es/string/to-well-formed.js';
//╚===--------------- ---------------===╝//
/* 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';
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 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 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');
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 <Element {...allProps} />;
const queryParams = Object.fromEntries(searchParams?.entries() || []);
return <Element {...rest} {...params} query={queryParams} />;
};
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 {};
},
render : function (){
return (
<Router location={this.props.url}>
<div className='homebrew'>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
</Routes>
</div>
</Router>
);
}
});
return (
<Router location={url}>
<div className='homebrew'>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={userThemes}/> } />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={brews} />} />
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
<Route path='/changelog' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/migrate' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={brew} accountDetails={brew.accountDetails} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={brew} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={brew} />} />
</Routes>
</div>
</Router>
);
};
module.exports = Homebrew;

View File

@@ -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');
@@ -20,7 +21,7 @@ const Account = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
@@ -190,8 +191,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
}
}));
},
@@ -247,6 +249,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,21 +261,25 @@ 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.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
brew.patches = makePatches(this.savedBrew.text, brew.text);
brew.hash = await md5(this.savedBrew.text);
brew.text = undefined;
brew.textBin = undefined;
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(encodeURI(this.savedBrew.text), encodeURI(brew.text)));
brew.hash = await md5(this.savedBrew.text);
//brew.text = undefined; - Temporary parallel path
brew.textBin = undefined;
const compressedBrew = gzipSync(strToU8(JSON.stringify(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}`)
.send(brew)
.set('Content-Encoding', 'gzip')
.set('Content-Type', 'application/json')
.send(compressedBrew)
.catch((err)=>{
console.log('Error Updating Local Brew');
this.setState({ error: err });
@@ -293,8 +302,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() });
});

View File

@@ -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

View File

@@ -15,7 +15,7 @@ const AccountNavItem = require('../../navbar/account.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');

View File

@@ -14,7 +14,7 @@ const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');

View File

@@ -12,7 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
import request from '../../utils/request-middleware.js';

View File

@@ -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(),

1169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.19.1",
"version": "3.19.3",
"type": "module",
"engines": {
"npm": "^10.8.x",
@@ -72,7 +72,7 @@
"lines": 50
},
"server/homebrew.api.js": {
"statements": 70,
"statements": 60,
"branches": 50,
"functions": 65,
"lines": 70
@@ -88,13 +88,14 @@
"@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",
"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",
@@ -102,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",
@@ -120,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",
@@ -136,17 +138,17 @@
"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",
"jest": "^30.0.4",
"jest": "^30.0.5",
"jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0",
"stylelint": "^16.21.1",
"stylelint": "^16.22.0",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended": "^16.0.0",
"supertest": "^7.1.1"
"supertest": "^7.1.4"
}
}

View File

@@ -28,6 +28,7 @@
"codemirror/addon/hint/show-hint.js",
"moment",
"superagent",
"@sanity/diff-match-patch"
"@sanity/diff-match-patch",
"fflate"
]
}

View File

@@ -383,6 +383,7 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,
title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.',
image : req.brew.thumbnail || defaultMetaTags.image,
locale : req.brew.lang,
type : 'article'
};

View File

@@ -0,0 +1,66 @@
import forceSSL from './forcessl.mw';
describe('Tests for ForceSSL middleware', ()=>{
let originalEnv;
let nextFn;
let req = {};
let res = {};
beforeEach(()=>{
originalEnv = process.env.NODE_ENV;
nextFn = jest.fn();
req = {
header : ()=>{ return 'http'; },
get : ()=>{ return 'test'; },
url : 'URL'
};
res = {
redirect : jest.fn()
};
});
afterEach(()=>{
process.env.NODE_ENV = originalEnv;
jest.clearAllMocks();
});
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';
forceSSL(req, res, nextFn);
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();
});
});

View File

@@ -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';
@@ -48,6 +48,20 @@ const api = {
}
id = id.slice(googleId.length);
}
// 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 };
}
// 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 };
}
return { id, googleId };
},
//Get array of any of this user's brews tagged with `meta:theme`
@@ -341,22 +355,44 @@ const api = {
const brewFromServer = req.brew;
splitTextStyleAndMetadata(brewFromServer);
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.` }));
}
brewFromServer.text = brewFromServer.text.normalize('NFC');
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}`);
//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.` }));
}
try {
const patches = parsePatch(brewFromClient.patches);
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[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 : 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 = applyPatches(brewFromClient.patches, brewFromServer.text)[0];
brew.text = api.mergeBrewText(brew);
const googleId = brew.googleId;
@@ -501,4 +537,4 @@ router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
export default api;
export default api;

View File

@@ -99,18 +99,87 @@ 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 right length but does not match pattern', ()=>{
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 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', ()=>{
@@ -1052,4 +1121,83 @@ 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, text: '' };
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.\"}');
});
// 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,
// };
// 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
})
);
});
});
});

View File

@@ -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;
export default generateAccessToken;

27
server/token.spec.js Normal file
View File

@@ -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');
});
});

View File

@@ -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
};

View File

@@ -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];

View File

@@ -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 = `<a href="javascript:alert('This is a JavaScript injection via href attribute')">Click me</a>`;
const rendered = safeHTML(source);