diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
index a59a50f74..162728c58 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
@@ -7,9 +7,11 @@ const request = require('../../utils/request-middleware.js');
const Nav = require('naturalcrit/nav/nav.jsx');
const Combobox = require('client/components/combobox.jsx');
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
-
+const htmlimg = require('html-to-image');
const Themes = require('themes/themes.json');
const validations = require('./validations.js');
+const base64url = require('base64-url');
+
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
@@ -30,6 +32,7 @@ const MetadataEditor = createClass({
title : '',
description : '',
thumbnail : '',
+ thumbnailSm : null,
tags : [],
published : false,
authors : [],
@@ -55,9 +58,59 @@ const MetadataEditor = createClass({
});
},
+
+ thumbnailCapture : async function() {
+
+ function urlReplacer(urlMatch, url) {
+ return (`url(/xssp/${base64url.encode(url)})`);
+ }
+ const bR = parent.document.getElementById('BrewRenderer');
+ const brewRenderer = bR.contentDocument || bR.contentWindow.document;
+ const pageOne = brewRenderer.getElementsByClassName('page')[0];
+ const topPage = pageOne.cloneNode(true);
+ pageOne.parentNode.appendChild(topPage);
+ // Walk through Top Page's Source and convert all Images to inline data *in* topPage.
+ const srcImages = pageOne.getElementsByTagName('img');
+ const topImages = topPage.getElementsByTagName('img');
+ const topLinks = brewRenderer.getElementsByTagName('link');
+ const topStyles = brewRenderer.getElementsByTagName('style');
+ // These two should start off with identical contents.
+ for (let imgPos = 0; imgPos < srcImages.length; imgPos++) {
+ topImages[imgPos].src = `/xssp/${base64url.encode(srcImages[imgPos].src)}`;
+ }
+ for (let linkPos = 0; linkPos < topLinks.length; linkPos++) {
+ topLinks[linkPos].href = `/xssp/${base64url.encode(topLinks[linkPos].href)}`;
+ }
+ for (let stylePos = 0; stylePos < topStyles.length; stylePos++) {
+ const urlRegex = /url\(([^\'\"].*[^\'\"])\)/gs;
+ const urlRegexWrapped = /url\(\'(.*)\'\)/gs;
+ topStyles[stylePos].innerText = topStyles[stylePos].innerText.replace(urlRegex, urlReplacer);
+ topStyles[stylePos].innerText = topStyles[stylePos].innerText.replace(urlRegexWrapped, urlReplacer);
+ }
+ const props = this.props;
+
+ const clientHeightLg = topPage.clientHeight * 0.5;
+ const clientWidthSm = topPage.clientWidth * (115/topPage.clientHeight);
+ const clientWidthLg = topPage.clientWidth * 0.5;
+
+ htmlimg.toPng(topPage, { canvasHeight : clientHeightLg, canvasWidth : clientWidthLg
+ }).then(function(dataURL){
+ props.metadata.thumbnailLg = dataURL;
+ htmlimg.toJpeg(topPage, { canvasHeight : 115, canvasWidth : clientWidthSm, quality : 0.95
+ }).then(function(dataURL){
+ props.metadata.thumbnail = 'Page 1';
+ props.metadata.thumbnailSm = dataURL;
+ props.onChange(props.metadata);
+ topPage.remove();
+ });
+ props.onChange(props.metadata);
+ });
+ },
+
renderThumbnail : function(){
if(!this.state.showThumbnail) return;
- return ;
+ const imgURL = this.props.metadata.thumbnail.startsWith('Page 1') ? this.props.metadata.thumbnailSm : this.props.metadata.thumbnail;
+ return ;
},
handleFieldChange : function(name, e){
@@ -332,6 +385,9 @@ const MetadataEditor = createClass({
+
{this.renderThumbnail()}
diff --git a/client/homebrew/editor/metadataEditor/validations.js b/client/homebrew/editor/metadataEditor/validations.js
index 32c8131f6..0f5a99a38 100644
--- a/client/homebrew/editor/metadataEditor/validations.js
+++ b/client/homebrew/editor/metadataEditor/validations.js
@@ -16,7 +16,9 @@ module.exports = {
(value)=>{
if(value?.length == 0){return null;}
try {
- Boolean(new URL(value));
+ if(value != 'Page 1') {
+ Boolean(new URL(value));
+ }
return null;
} catch (e) {
return 'Must be a valid URL';
diff --git a/config/default.json b/config/default.json
index 70c90593e..ab68d9ffa 100644
--- a/config/default.json
+++ b/config/default.json
@@ -5,5 +5,7 @@
"web_port" : 8000,
"enable_v3" : true,
"local_environments" : ["docker", "local"],
- "publicUrl" : "https://homebrewery.naturalcrit.com"
+ "publicUrl" : "https://homebrewery.naturalcrit.com",
+ "proxyHost" : "https://172.17.0.1",
+ "proxyPort" : 3128
}
diff --git a/package-lock.json b/package-lock.json
index d9c862149..be221da67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@babel/preset-env": "^7.24.7",
"@babel/preset-react": "^7.24.7",
"@googleapis/drive": "^8.10.0",
+ "base64-url": "^2.3.3",
"body-parser": "^1.20.2",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
@@ -25,8 +26,10 @@
"expr-eval": "^2.0.2",
"express": "^4.19.2",
"express-async-handler": "^1.2.0",
+ "express-requests-logger": "^4.0.0",
"express-static-gzip": "2.1.7",
"fs-extra": "11.2.0",
+ "html-to-image": "^1.11.11",
"js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
@@ -3964,6 +3967,14 @@
}
]
},
+ "node_modules/base64-url": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-2.3.3.tgz",
+ "integrity": "sha512-dLMhIsK7OplcDauDH/tZLvK7JmUZK3A7KiQpjNzsBrM6Etw7hzNI1tLEywqJk9NnwkgWuFKSlx/IUO7vF6Mo8Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
@@ -4364,6 +4375,23 @@
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
"integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ=="
},
+ "node_modules/bunyan": {
+ "version": "1.8.15",
+ "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz",
+ "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==",
+ "engines": [
+ "node >=0.10.0"
+ ],
+ "bin": {
+ "bunyan": "bin/bunyan"
+ },
+ "optionalDependencies": {
+ "dtrace-provider": "~0.8",
+ "moment": "^2.19.3",
+ "mv": "~2",
+ "safe-json-stringify": "~1"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -5472,6 +5500,19 @@
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.5.tgz",
"integrity": "sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA=="
},
+ "node_modules/dtrace-provider": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
+ "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "nan": "^2.14.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@@ -6270,6 +6311,16 @@
"resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz",
"integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w=="
},
+ "node_modules/express-requests-logger": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/express-requests-logger/-/express-requests-logger-4.0.0.tgz",
+ "integrity": "sha512-NHQptnDY0fceiTSWLnW0dbJSFlrvbFpCGHmY6LsTMmJLgkyO3x8qAJ+EsryQRMga20YH8Ynt/vnmg23QP07h1Q==",
+ "dependencies": {
+ "bunyan": "^1.8.14",
+ "flat": "^5.0.2",
+ "lodash": "^4.17.14"
+ }
+ },
"node_modules/express-static-gzip": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.7.tgz",
@@ -6540,6 +6591,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
"node_modules/flat-cache": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz",
@@ -7259,6 +7318,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/html-to-image": {
+ "version": "1.11.11",
+ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz",
+ "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA=="
+ },
"node_modules/htmlescape": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz",
@@ -10569,6 +10633,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "optional": true,
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -10807,6 +10883,50 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
+ "node_modules/mv": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
+ "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==",
+ "optional": true,
+ "dependencies": {
+ "mkdirp": "~0.5.1",
+ "ncp": "~2.0.0",
+ "rimraf": "~2.4.0"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/mv/node_modules/glob": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+ "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "optional": true,
+ "dependencies": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mv/node_modules/rimraf": {
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
+ "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "optional": true,
+ "dependencies": {
+ "glob": "^6.0.1"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
"node_modules/nan": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
@@ -10984,6 +11104,15 @@
"node": ">=10"
}
},
+ "node_modules/ncp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
+ "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==",
+ "optional": true,
+ "bin": {
+ "ncp": "bin/ncp"
+ }
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -12598,6 +12727,12 @@
}
]
},
+ "node_modules/safe-json-stringify": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
+ "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
+ "optional": true
+ },
"node_modules/safe-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
@@ -14578,14 +14713,12 @@
}
},
"node_modules/vitreum": {
- "version": "6.0.4",
- "resolved": "git+https://git@github.com/calculuschild/vitreum.git#9d55fd6fb7e85e7070de798c4f9d5b983c1b7dba",
- "integrity": "sha512-6b5A4XEXnpyl6JDRWWOhe5lHxtzMVqNA+0Xrl7mPXqh7cYwTUm0yhkYEUkV+mz7rrHTpH7bYEWYsWEE/qTZMpg==",
- "hasInstallScript": true,
+ "version": "6.0.1",
+ "resolved": "git+https://git@github.com/calculuschild/vitreum.git#49994da4055f914269318b2b9ae953707aa771b6",
"license": "MIT",
"dependencies": {
"browserify": "^16.5.0",
- "fs-extra": "^9.0.1",
+ "fs-extra": "^9.0.0",
"livereload": "^0.9.1",
"nodemon": "^2.0.2",
"source-map-support": "^0.5.16",
@@ -14596,8 +14729,8 @@
"@babel/core": "^7.9.0",
"@babel/preset-react": "^7.9.4",
"less": "^3.11.1",
- "react": "^18.3.1",
- "react-dom": "^18.3.1"
+ "react": ">=16.13.1",
+ "react-dom": ">=16.13.1"
}
},
"node_modules/vitreum/node_modules/fs-extra": {
diff --git a/package.json b/package.json
index b5b7824b3..1ce9d9a6a 100644
--- a/package.json
+++ b/package.json
@@ -87,6 +87,7 @@
"@babel/preset-env": "^7.24.7",
"@babel/preset-react": "^7.24.7",
"@googleapis/drive": "^8.10.0",
+ "base64-url": "^2.3.3",
"body-parser": "^1.20.2",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
@@ -97,8 +98,10 @@
"expr-eval": "^2.0.2",
"express": "^4.19.2",
"express-async-handler": "^1.2.0",
+ "express-requests-logger": "^4.0.0",
"express-static-gzip": "2.1.7",
"fs-extra": "11.2.0",
+ "html-to-image": "^1.11.11",
"js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
diff --git a/server/app.js b/server/app.js
index e26c98f54..7c752f046 100644
--- a/server/app.js
+++ b/server/app.js
@@ -5,6 +5,8 @@ process.chdir(`${__dirname}/..`);
const _ = require('lodash');
const jwt = require('jwt-simple');
const express = require('express');
+
+
const yaml = require('js-yaml');
const app = express();
const config = require('./config.js');
@@ -14,6 +16,8 @@ const GoogleActions = require('./googleActions.js');
const serveCompressedStaticAssets = require('./static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler');
+const requests = require('request');
+const base64url = require('base64-url');
const { DEFAULT_BREW } = require('./brewDefaults.js');
@@ -77,6 +81,35 @@ app.get('/robots.txt', (req, res)=>{
return res.sendFile(`robots.txt`, { root: process.cwd() });
});
+// The proxy endpoint
+app.get('/xssp/:id', (req, res)=>{
+ // Presumably needs some sanitization
+ try {
+ // Test :id - aHR0cHM6Ly9zLW1lZGlhLWNhY2hlLWFrMC5waW5pbWcuY29tLzczNngvNGEvODEvNzkvNGE4MTc5NDYyY2ZkZjM5MDU0YTQxOGVmZDRjYjc0M2UuanBn
+ const decodedURL = base64url.decode(req.params.id);
+ const rOptions = {
+ host : config.get('proxyHost'),
+ port : config.get('proxyPort'),
+ timeout : 30000, // 30s
+ headers : {
+ 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:48.0) Gecko/20100101 Firefox/48.0'
+ },
+ url : decodedURL,
+ encoding : null
+ };
+
+ requests(rOptions, function (error, response, body) {
+ res.set({
+ 'Content-Type' : response.headers['content-type'],
+ });
+ res.send(response.body);
+ });
+ } catch (e) {
+ console.log(e);
+ return;
+ }
+});
+
//Home page
app.get('/', (req, res, next)=>{
req.brew = {
diff --git a/server/homebrew.model.js b/server/homebrew.model.js
index 36c9aa192..7c2459220 100644
--- a/server/homebrew.model.js
+++ b/server/homebrew.model.js
@@ -21,6 +21,7 @@ const HomebrewSchema = mongoose.Schema({
invitedAuthors : [String],
published : { type: Boolean, default: false },
thumbnail : { type: String, default: '' },
+ thumbnailSm : { type: String, default: '' },
createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now },
diff --git a/server/middleware/content-negotiation.js b/server/middleware/content-negotiation.js
index 201e64a25..551b064b4 100644
--- a/server/middleware/content-negotiation.js
+++ b/server/middleware/content-negotiation.js
@@ -1,12 +1,13 @@
module.exports = (req, res, next)=>{
- const isImageRequest = req.get('Accept')?.split(',')
- ?.filter((h)=>!h.includes('q='))
- ?.every((h)=>/image\/.*/.test(h));
- if(isImageRequest) {
- return res.status(406).send({
- message : 'Request for image at this URL is not supported'
- });
+ if(! req.url.startsWith('/xssp/')) {
+ const isImageRequest = req.get('Accept')?.split(',')
+ ?.filter((h)=>!h.includes('q='))
+ ?.every((h)=>/image\/.*/.test(h));
+ if(isImageRequest) {
+ return res.status(406).send({
+ message : 'Request for image at this URL is not supported'
+ });
+ }
}
-
next();
};
\ No newline at end of file