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