diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index a1490ed28..529c0d9ee 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -63,6 +63,9 @@ const SharePage = createClass({ source + + download + diff --git a/client/homebrew/pages/userPage/brewItem/brewItem.jsx b/client/homebrew/pages/userPage/brewItem/brewItem.jsx index b7b13ff4c..b24bad363 100644 --- a/client/homebrew/pages/userPage/brewItem/brewItem.jsx +++ b/client/homebrew/pages/userPage/brewItem/brewItem.jsx @@ -78,6 +78,19 @@ const BrewItem = createClass({ ; }, + renderDownloadLink : function(){ + if(!this.props.brew.shareId) return; + + let shareLink = this.props.brew.shareId; + if(this.props.brew.googleId) { + shareLink = this.props.brew.googleId + shareLink; + } + + return + + ; + }, + renderGoogleDriveIcon : function(){ if(!this.props.brew.gDrive) return; @@ -109,6 +122,7 @@ const BrewItem = createClass({
{this.renderShareLink()} {this.renderEditLink()} + {this.renderDownloadLink()} {this.renderDeleteBrewLink()}
; diff --git a/client/homebrew/pages/userPage/brewItem/brewItem.less b/client/homebrew/pages/userPage/brewItem/brewItem.less index 6ab0a893c..efae9d8d3 100644 --- a/client/homebrew/pages/userPage/brewItem/brewItem.less +++ b/client/homebrew/pages/userPage/brewItem/brewItem.less @@ -7,7 +7,7 @@ box-sizing : border-box; overflow : hidden; width : 48%; - min-height : 80px; + min-height : 105px; margin-right : 15px; margin-bottom : 15px; padding : 5px 15px 5px 8px; diff --git a/package-lock.json b/package-lock.json index 011c61d05..43edad909 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,16 +58,16 @@ "integrity": "sha512-VhgqKOWYVm7lQXlvbJnWOzwfAQATd2nV52koT0HZ/LdDH0m4DUDwkKYsH+IwpXb+bKPyBJzawA4I6nBKqZcpQw==" }, "@babel/core": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.8.tgz", - "integrity": "sha512-oYapIySGw1zGhEFRd6lzWNLWFX2s5dA/jm+Pw/+59ZdXtjyIuwlXbrId22Md0rgZVop+aVoqow2riXhBLNyuQg==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.10.tgz", + "integrity": "sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==", "requires": { "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.0", - "@babel/helper-compilation-targets": "^7.13.8", + "@babel/generator": "^7.13.9", + "@babel/helper-compilation-targets": "^7.13.10", "@babel/helper-module-transforms": "^7.13.0", - "@babel/helpers": "^7.13.0", - "@babel/parser": "^7.13.4", + "@babel/helpers": "^7.13.10", + "@babel/parser": "^7.13.10", "@babel/template": "^7.12.13", "@babel/traverse": "^7.13.0", "@babel/types": "^7.13.0", @@ -85,10 +85,20 @@ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.8.tgz", "integrity": "sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog==" }, + "@babel/generator": { + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", + "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "requires": { + "@babel/types": "^7.13.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, "@babel/helper-compilation-targets": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.8.tgz", - "integrity": "sha512-pBljUGC1y3xKLn1nrx2eAhurLMA8OqBtBP/JwG4U8skN7kf8/aqwwxpV1N6T0e7r6+7uNitIa/fUxPFagSXp3A==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz", + "integrity": "sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA==", "requires": { "@babel/compat-data": "^7.13.8", "@babel/helper-validator-option": "^7.12.17", @@ -96,20 +106,10 @@ "semver": "^6.3.0" } }, - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" - }, - "@babel/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz", - "integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==", - "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } + "@babel/parser": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.10.tgz", + "integrity": "sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ==" }, "convert-source-map": { "version": "1.7.0", @@ -580,30 +580,13 @@ } }, "@babel/helpers": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.0.tgz", - "integrity": "sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.10.tgz", + "integrity": "sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==", "requires": { "@babel/template": "^7.12.13", "@babel/traverse": "^7.13.0", "@babel/types": "^7.13.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" - }, - "@babel/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz", - "integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==", - "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/highlight": { @@ -1888,18 +1871,18 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/mongodb": { - "version": "3.6.8", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.8.tgz", - "integrity": "sha512-8qNbL5/GFrljXc/QijcuQcUMYZ1iWNcqnJ6tneROwbfU0LsAjQ9bmq3aHi5lWXM4cyBPd2F/n9INAk/pZZttHw==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.9.tgz", + "integrity": "sha512-2XSGr/+IOKeFQ5tU9ATcIiIr7bpHqWyOXNGLOOhp0kg2NnfEvoKZF1SZ25j4zvJRqM2WeSUjfWSvymFJ3HBGJQ==", "requires": { "@types/bson": "*", "@types/node": "*" } }, "@types/node": { - "version": "14.14.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz", - "integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==" + "version": "14.14.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.33.tgz", + "integrity": "sha512-oJqcTrgPUF29oUP8AsUqbXGJNuPutsetaa9kTQAQce5Lx5dTYWV02ScBiT/k1BX/Z7pKeqedmvp39Wu4zR7N7g==" }, "JSONStream": { "version": "1.3.5", @@ -5478,9 +5461,9 @@ } }, "mongoose": { - "version": "5.11.18", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.18.tgz", - "integrity": "sha512-RsrPR9nhkXZbO3ml0DcmdbfeMvFNhgFrP81S6o1P+lFnDTNEKYnGNRCIL+ojD69wj7H5jJaAdZ0SJ5IlKxCHqw==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.12.0.tgz", + "integrity": "sha512-s0Qpgf5lOk3AXtKnE+FA0HZhFKa2hesGVcTmx1wfTQ+7Q7ph0E79B6KUp1ZQERQyCwuE8WQ4wWllEhd7VPkxOg==", "requires": { "@types/mongodb": "^3.5.27", "bson": "^1.1.4", @@ -6592,6 +6575,14 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "requires": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "saslprep": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", @@ -7333,6 +7324,14 @@ "nopt": "~1.0.10" } }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "requires": { + "utf8-byte-length": "^1.0.1" + } + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -7589,6 +7588,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" + }, "util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", diff --git a/package.json b/package.json index edf794350..edda8ed4f 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ ] }, "dependencies": { - "@babel/core": "^7.13.8", + "@babel/core": "^7.13.10", "@babel/preset-env": "^7.13.9", "@babel/preset-react": "^7.12.13", "body-parser": "^1.19.0", @@ -56,7 +56,7 @@ "less": "^3.13.1", "lodash": "^4.17.21", "moment": "^2.29.1", - "mongoose": "^5.11.18", + "mongoose": "^5.12.0", "nanoid": "3.1.21", "markedLegacy": "npm:marked@^0.3.19", "marked": "2.0.1", @@ -67,6 +67,7 @@ "react-dom": "^16.14.0", "react-frame-component": "4.1.3", "react-router-dom": "5.2.0", + "sanitize-filename": "1.6.3", "superagent": "^6.1.0", "vitreum": "github:calculuschild/vitreum#21a8e1c9421f1d3a3b474c12f480feb2fbd28c5b" }, diff --git a/server.js b/server.js index ca2f6f17f..2b460ccf2 100644 --- a/server.js +++ b/server.js @@ -1,4 +1,4 @@ -/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/ +/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ const _ = require('lodash'); const jwt = require('jwt-simple'); const express = require('express'); @@ -7,6 +7,7 @@ const app = express(); const homebrewApi = require('./server/homebrew.api.js'); const GoogleActions = require('./server/googleActions.js'); const serveCompressedStaticAssets = require('./server/static-assets.mv.js'); +const sanitizeFilename = require('sanitize-filename'); app.use('/', serveCompressedStaticAssets(`${__dirname}/build`)); @@ -64,6 +65,7 @@ app.get('/robots.txt', (req, res)=>{ return res.sendFile(`${__dirname}/robots.txt`); }); + //Source page app.get('/source/:id', (req, res)=>{ if(req.params.id.length > 12) { @@ -71,8 +73,13 @@ app.get('/source/:id', (req, res)=>{ const shareId = req.params.id.slice(-12); GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share') .then((brew)=>{ - const text = brew.text.replaceAll('<', '<').replaceAll('>', '>'); - return res.send(`
${text}
`); + const replaceStrings = { '&': '&', '<': '<', '>': '>' }; + let text = brew.text; + for (const replaceStr in replaceStrings) { + text = text.replaceAll(replaceStr, replaceStrings[replaceStr]); + } + text = `
${text}
`; + res.status(200).send(text); }) .catch((err)=>{ console.log(err); @@ -80,14 +87,60 @@ app.get('/source/:id', (req, res)=>{ }); } else { HomebrewModel.get({ shareId: req.params.id }) - .then((brew)=>{ - const text = brew.text.replaceAll('<', '<').replaceAll('>', '>'); - return res.send(`
${text}
`); - }) - .catch((err)=>{ - console.log(err); - return res.status(404).send('Could not find Homebrew with that id'); + .then((brew)=>{ + const replaceStrings = { '&': '&', '<': '<', '>': '>' }; + let text = brew.text; + for (const replaceStr in replaceStrings) { + text = text.replaceAll(replaceStr, replaceStrings[replaceStr]); + } + text = `
${text}
`; + res.status(200).send(text); + }) + .catch((err)=>{ + console.log(err); + return res.status(404).send('Could not find Homebrew with that id'); + }); + } +}); + +//Download brew source page +app.get('/download/:id', (req, res)=>{ + const prefix = 'HB - '; + + if(req.params.id.length > 12) { + const googleId = req.params.id.slice(0, -12); + const shareId = req.params.id.slice(-12); + GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share') + .then((brew)=>{ + let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', ''); + if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; }; + res.set({ + 'Cache-Control' : 'no-cache', + 'Content-Type' : 'text/plain', + 'Content-Disposition' : `attachment; filename="${fileName}.txt"` }); + res.status(200).send(brew.text); + }) + .catch((err)=>{ + console.log(err); + return res.status(400).send('Can\'t get brew from Google'); + }); + } else { + HomebrewModel.get({ shareId: req.params.id }) + .then((brew)=>{ + let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', ''); + if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; }; + res.set({ + 'Cache-Control' : 'no-cache', + 'Content-Type' : 'text/plain', + 'Content-Disposition' : `attachment; filename="${fileName}.txt"` + }); + res.status(200).send(brew.text); + }) + .catch((err)=>{ + console.log(err); + return res.status(404).send('Could not find Homebrew with that id'); + }); } }); diff --git a/server/googleActions.js b/server/googleActions.js index 1a8d656ac..bd288ceea 100644 --- a/server/googleActions.js +++ b/server/googleActions.js @@ -259,8 +259,12 @@ GoogleActions = { throw ('Share ID does not match'); } - //Access actual file with service account. Just api key is causing "automated query" errors. - const keys = JSON.parse(config.get('service_account')); + //Access file using service account. Using API key only causes "automated query" lockouts after a while. + + const keys = typeof(config.get('service_account')) == 'string' ? + JSON.parse(config.get('service_account')) : + config.get('service_account'); + const serviceAuth = google.auth.fromJSON(keys); serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];