diff --git a/middleware/cors.js b/middleware/cors.js index 894bcf9f..58910309 100644 --- a/middleware/cors.js +++ b/middleware/cors.js @@ -1,8 +1,8 @@ module.exports = (_req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Origin', '*') res.header( 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept' - ); - next(); -}; + ) + next() +} diff --git a/middleware/load-options.js b/middleware/load-options.js index 5879c25e..a624b9f6 100644 --- a/middleware/load-options.js +++ b/middleware/load-options.js @@ -1,63 +1,63 @@ -const currentYear = new Date().getFullYear(); +const currentYear = new Date().getFullYear() module.exports = (req, res, next) => { - const parts = req.url.split('/'); + const parts = req.url.split('/') res.locals.options = parts.reduce( (acc, curr) => { - if (!curr) return acc; + if (!curr) return acc - let match = curr.match(/^@?(\d{4})$/) || []; + let match = curr.match(/^@?(\d{4})$/) || [] if (match.length) { // Pinned year if (curr.startsWith('@')) { - acc.pinnedYear = parseInt(curr.substr(1), 10); + acc.pinnedYear = parseInt(curr.substr(1), 10) } else { - acc.startYear = parseInt(curr, 10); + acc.startYear = parseInt(curr, 10) } - return acc; + return acc } - match = curr.match(/^(\d{4})-(\d{4})$/) || []; + match = curr.match(/^(\d{4})-(\d{4})$/) || [] if (match.length) { - acc.startYear = parseInt(match[1], 10); - acc.endYear = parseInt(match[2], 10); + acc.startYear = parseInt(match[1], 10) + acc.endYear = parseInt(match[2], 10) - return acc; + return acc } if (curr.startsWith('license')) { acc.format = curr .split('.') .pop() - .trim(); - return acc; + .trim() + return acc } if (curr.startsWith('+')) { - acc.license = curr.substr(1).toUpperCase(); - return acc; + acc.license = curr.substr(1).toUpperCase() + return acc } - acc.sha = curr; // not actually supported now - 2019-06-19 - return acc; + acc.sha = curr // not actually supported now - 2019-06-19 + return acc }, { format: 'html', startYear: null, endYear: currentYear, - sha: null, + sha: null } - ); + ) if (res.locals.options.sha) { res.setHeader( 'X-note', 'SHA and commit pinning is no longer supported, showing you latest release' - ); + ) } - next(); -}; + next() +} diff --git a/middleware/load-user.js b/middleware/load-user.js index eb4f9424..0f93d492 100644 --- a/middleware/load-user.js +++ b/middleware/load-user.js @@ -1,35 +1,35 @@ -const fs = require('fs-extra'); -const path = require('path'); +const fs = require('fs-extra') +const path = require('path') module.exports = async (req, res, next) => { - const id = req.hostname.split('.')[0]; - res.locals.id = id; + const id = req.hostname.split('.')[0] + res.locals.id = id if (req.method.toUpperCase() !== 'GET') { - return next(); + return next() } // Otherwise load up the user json file res.locals.user = { - copyright: '', - }; + copyright: '' + } try { const data = await fs.readFile( path.join(__dirname, '..', 'users', `${id}.json`), 'utf8' - ); - res.locals.user = { ...res.locals.user, ...JSON.parse(data) }; + ) + res.locals.user = { ...res.locals.user, ...JSON.parse(data) } } catch ({ code, message }) { if (code !== 'ENOENT') { res .code(500) .send( `An internal error occurred - open an issue on https://github.com/remy/mit-license with the following information: ${message}` - ); - return; + ) + return } } - next(); -}; + next() +} diff --git a/package.json b/package.json index c51cd738..101c86ca 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dev": "nodemon .", "serve": "node server.js", "test": "node test.js", - "lint": "eslint server.js middleware/*.js routes/*.js --color" + "lint": "standard" }, "bugs": { "url": "https://github.com/remy/mit-license/issues" @@ -41,17 +41,15 @@ "postcss-middleware": "^1.1.4", "postcss-preset-env": "^6.7.0", "serve-favicon": "^2.5.0", - "temp-dir": "^2.0.0" + "temp-dir": "^2.0.0", + "yn": "^4.0.0" }, "devDependencies": { - "@remy/eslint": "^3.2.2", - "babel-eslint": "^10.0.3", "css": "^2.2.4", - "eslint": "^6.8.0", - "eslint-plugin-node": "^11.0.0", "has-flag": "^4.0.0", - "husky": "^3.1.0", - "nodemon": "^2.0.2" + "husky": "^4.0.7", + "nodemon": "^2.0.2", + "standard": "^14.3.1" }, "resolutions": { "postcss-middleware/vinyl-fs/glob-stream/micromatch/braces": "^3.0.2" diff --git a/routes/get.js b/routes/get.js index ffd2e3ff..15d0f937 100644 --- a/routes/get.js +++ b/routes/get.js @@ -1,48 +1,48 @@ -const md5 = require('md5'); -const path = require('path'); -const { stripTags, escapeTags, unescapeTags } = require('./utils'); -const _ = require('lodash'); +const md5 = require('md5') +const path = require('path') +const { stripTags, escapeTags, unescapeTags } = require('./utils') +const _ = require('lodash') -function getCopyrightHTML(user, plain) { - let html = ''; +function getCopyrightHTML (user, plain) { + let html = '' const name = _.isString(user) ? user : plain ? user.name || user.copyright - : escapeTags(user.name || user.copyright); + : escapeTags(user.name || user.copyright) if (user.url) { - html = `${name}`; + html = `${name}` } else { - html = name; + html = name } if (user.email) { html += ` <${ plain ? user.email : escapeTags(user.email) - }>`; + }>` } - return html; + return html } module.exports = (req, res) => { - const { user, options } = res.locals; - let name; - let gravatar; + const { user, options } = res.locals + let name + let gravatar // No error and valid if (user.copyright) { if (_.isString(user.copyright)) { - name = getCopyrightHTML(user, options.format !== 'html'); + name = getCopyrightHTML(user, options.format !== 'html') } else if (_.isArray(user.copyright) && user.copyright.every(val => _.isString(val))) { // Supports: ['Remy Sharp', 'Richie Bendall'] name = user.copyright .map(v => (options.format !== 'html' ? v : escapeTags(v))) - .join(', '); + .join(', ') } else { - name = user.copyright.map(getCopyrightHTML).join(', '); + name = user.copyright.map(getCopyrightHTML).join(', ') } } @@ -50,47 +50,47 @@ module.exports = (req, res) => { // Supports regular format gravatar = `Profile image`; + )}" />` } else if (_.isObject(user.copyright[0]) && user.gravatar) { // Supports multi-user format gravatar = `Profile image`; + )}" />` } const year = options.pinnedYear ? options.pinnedYear - : [options.startYear, options.endYear].filter(Boolean).join('-'); - const license = (options.license || user.license || 'MIT').toUpperCase(); - const format = options.format || user.format || 'html'; + : [options.startYear, options.endYear].filter(Boolean).join('-') + const license = (options.license || user.license || 'MIT').toUpperCase() + const format = options.format || user.format || 'html' const args = { info: `${year} ${name}`, theme: user.theme || 'default', - gravatar, - }; + gravatar + } - const filename = path.join(__dirname, '..', 'licenses', license); + const filename = path.join(__dirname, '..', 'licenses', license) req.app.render(filename, args, (error, content) => { if (error) { - res.status(500).send(error); - return; + res.status(500).send(error) + return } if (format === 'txt') { - const plain = content.match(/
(.*)<\/article>/ms)[1]; + const plain = content.match(/
(.*)<\/article>/ms)[1] res .set('Content-Type', 'text/plain; charset=UTF-8') - .send(unescapeTags(stripTags(plain)).trim()); - return; + .send(unescapeTags(stripTags(plain)).trim()) + return } if (format === 'html') { - res.send(content); - return; + res.send(content) + return } - res.json({ ...user, ...options }); - }); -}; + res.json({ ...user, ...options }) + }) +} diff --git a/routes/post.js b/routes/post.js index dae92717..a9dbf528 100644 --- a/routes/post.js +++ b/routes/post.js @@ -1,42 +1,44 @@ -const fs = require('fs-extra'); -const path = require('path'); -const btoa = require('btoa'); -const { version } = require(path.join(__dirname, '..', 'package.json')); -const _ = require('lodash'); +const fs = require('fs-extra') +const path = require('path') +const btoa = require('btoa') +const { version } = require(path.join(__dirname, '..', 'package.json')) +const _ = require('lodash') const github = require('@octokit/rest')({ // GitHub personal access token auth: process.env.github_token, // User agent with version from package.json - userAgent: `mit-license v${version}`, -}); -const { validDomainId } = require('./utils'); + userAgent: `mit-license v${version}` +}) +const yn = require('yn') -function getUserData({ query, body }) { +const { validDomainId } = require('./utils') + +function getUserData ({ query, body }) { // If query parameters provided - if (_.size(query) > 0) return query; + if (_.size(query) > 0) return query // If the data parsed as {'{data: "value"}': ''} - if (_.size(body) === 1 && !_.first(_.values(body))) return JSON.parse(_.first(_.keys(body))); + if (_.size(body) === 1 && !_.first(_.values(body))) return JSON.parse(_.first(_.keys(body))) // Fallback - return body; + return body } // HTTP POST API module.exports = async (req, res) => { - const { hostname } = req; + const { hostname } = req // Get different parts of hostname (example: remy.mit-license.org -> ['remy', 'mit-license', 'org']) - const params = hostname.split('.'); + const params = hostname.split('.') // This includes the copyright, year, etc. - const userData = getUserData(req); + const userData = getUserData(req) // If there isn't enough part of the hostname if (params.length < 2) { - res.status(400).send('Please specify a subdomain in the URL.'); - return; + res.status(400).send('Please specify a subdomain in the URL.') + return } // Extract the name from the URL - const id = _.first(params); + const id = _.first(params) if (!validDomainId(id)) { // Return a vague error intentionally @@ -44,31 +46,44 @@ module.exports = async (req, res) => { .status(400) .send( 'User already exists - to update values, please send a pull request on https://github.com/remy/mit-license' - ); + ) - return; + return } // Check if the user file exists in the users directory - const exists = await fs.pathExists(path.join(__dirname, '..', 'users', `${id}.json`)); + const exists = await fs.pathExists(path.join(__dirname, '..', 'users', `${id}.json`)) if (exists) { res .status(409) .send( 'User already exists - to update values, please send a pull request on https://github.com/remy/mit-license' - ); - return; + ) + return + } + + if (userData.gravatar) { + // Parse the string version of a boolean or similar + userData.gravatar = yn(userData.gravatar, { lenient: true }) + if (_.isUndefined(userData.gravatar)) { + res + .status(400) + .send( + 'The "gravatar" JSON property must be a boolean.' + ) + return + } } // File doesn't exist // If copyright property and key doesn't exist if (!userData.copyright) { - res.status(400).send('JSON requires "copyright" property and value'); - return; + res.status(400).send('JSON requires "copyright" property and value') + return } try { - const fileContent = JSON.stringify(userData, 0, 2); + const fileContent = JSON.stringify(userData, 0, 2) await github.repos.createOrUpdateFile({ owner: 'remy', @@ -78,18 +93,18 @@ module.exports = async (req, res) => { content: btoa(fileContent), committer: { name: 'MIT License Bot', - email: 'remy@leftlogic.com', - }, - }); + email: 'remy@leftlogic.com' + } + }) - await fs.writeFile(path.join(__dirname, '..', 'users', `${id}.json`), fileContent); + await fs.writeFile(path.join(__dirname, '..', 'users', `${id}.json`), fileContent) - res.status(201).send(`MIT license page created: https://${hostname}`); + res.status(201).send(`MIT license page created: https://${hostname}`) } catch (err) { res .status(500) .send( - `Unable to create new user - please send a pull request on https://github.com/remy/mit-license` - ); + 'Unable to create new user - please send a pull request on https://github.com/remy/mit-license' + ) } -}; +} diff --git a/routes/utils.js b/routes/utils.js index 72ae9d02..869c28af 100644 --- a/routes/utils.js +++ b/routes/utils.js @@ -1,16 +1,16 @@ -const _ = require('lodash'); +const _ = require('lodash') const tags = { '<': '<', '>': '>', - '&': '&', -}; -const untags = _.invert(tags); + '&': '&' +} +const untags = _.invert(tags) module.exports = { escapeTags: str => (str || '').replace(/[<>&]/g, m => tags[m]), unescapeTags: str => (str || '').replace(/(<|>|&)/g, m => untags[m]), stripTags: str => (str || '').replace(/<(?:.|\n)*?>/gm, ''), - validDomainId: str => /^[\w-_]+$/.test(str), -}; + validDomainId: str => /^[\w-_]+$/.test(str) +} diff --git a/server.js b/server.js index 8f4c5c1d..6df2a972 100644 --- a/server.js +++ b/server.js @@ -5,68 +5,68 @@ IMPORTANT: Set the `github_token` environment variable to a personal access to Server port: The `PORT` environment variable can also be set to control the port the server should be hosted on. */ -const express = require('express'); -const minify = require('express-minify'); -const favicon = require('serve-favicon'); -const postcssMiddleware = require('postcss-middleware'); -const tmpdir = require('temp-dir'); -const path = require('path'); +const express = require('express') +const minify = require('express-minify') +const favicon = require('serve-favicon') +const postcssMiddleware = require('postcss-middleware') +const tmpdir = require('temp-dir') +const path = require('path') // Server -var PORT = process.env.PORT || 8080; +var PORT = process.env.PORT || 8080 // Prepare application -const app = express(); +const app = express() app.use( minify({ - cache: tmpdir, + cache: tmpdir }) -); -app.use(favicon(path.join(__dirname, 'favicon.ico'))); -app.set('views', path.join(__dirname, '/licenses')); -app.set('view engine', 'ejs'); +) +app.use(favicon(path.join(__dirname, 'favicon.ico'))) +app.set('views', path.join(__dirname, '/licenses')) +app.set('view engine', 'ejs') // Setup static files -app.use('/robots.txt', express.static('robots.txt')); -app.use('/favicon.ico', express.static(`${__dirname}/favicon.ico`)); +app.use('/robots.txt', express.static('robots.txt')) +app.use('/favicon.ico', express.static(`${__dirname}/favicon.ico`)) app.use( '/themes', postcssMiddleware({ plugins: [ require('postcss-preset-env')({ overrideBrowserslist: '>= 0%', - stage: 0, - }), + stage: 0 + }) ], - src(req) { - return path.join(__dirname, 'themes', req.path); - }, + src (req) { + return path.join(__dirname, 'themes', req.path) + } }), express.static('themes') -); +) // Middleware // CORS -app.use(require('./middleware/cors')); +app.use(require('./middleware/cors')) // Parse URL-encoded bodies (as sent by HTML forms) app.use( express.urlencoded({ - extended: true, + extended: true }) -); +) // Parse JSON bodies (as sent by API clients) -app.use(express.json()); +app.use(express.json()) // Capture the id from the subdomain -app.use(require('./middleware/load-user')); -app.use(require('./middleware/load-options')); +app.use(require('./middleware/load-user')) +app.use(require('./middleware/load-options')) // HTTP POST API -app.post('/', require('./routes/post')); -app.get('/*', require('./routes/get')); +app.post('/', require('./routes/post')) +app.get('/*', require('./routes/get')) // Start listening for HTTP requests app.listen(PORT, () => { - console.log(`🚀 on http://localhost:${PORT}`); -}); + console.log(`🚀 on http://localhost:${PORT}`) +}) diff --git a/test.js b/test.js index a741e2ec..9593a3c2 100644 --- a/test.js +++ b/test.js @@ -1,68 +1,68 @@ -const path = require('path'); -const fs = require('fs-extra'); -const CSS = require('css'); -const { validDomainId } = require('./routes/utils'); -const hasFlag = require('has-flag'); +const path = require('path') +const fs = require('fs-extra') +const CSS = require('css') +const { validDomainId } = require('./routes/utils') +const hasFlag = require('has-flag') -function report(content, fix) { - console.error(content); - if (fix && hasFlag('--fix')) fix(); - process.exitCode = 1; +function report (content, fix) { + console.error(content) + if (fix && hasFlag('--fix')) fix() + process.exitCode = 1 } (async () => { - const users = await fs.readdir('users'); + const users = await fs.readdir('users') users.forEach(async user => { if (!user.endsWith('json')) { report(`${user} is not a json file`, () => fs.unlink(path.join('users', user), () => { }) - ); + ) } if (!validDomainId(user.replace('.json', ''))) { - report(`${user} is not a valid domain id.`); + report(`${user} is not a valid domain id.`) } try { - const data = await fs.readFile(path.join('users', user), 'utf8'); + const data = await fs.readFile(path.join('users', user), 'utf8') try { - const u = JSON.parse(data); + const u = JSON.parse(data) if (!u.locked && !u.copyright) { - report(`Copyright not specified in ${user}`); + report(`Copyright not specified in ${user}`) } if (u.version) { report(`Version tag found in ${user}`, () => { - delete u.version; - const stringified = `${JSON.stringify(u, 0, 2)}\n`; - fs.writeFile(path.join('users', user), stringified, () => { }); - }); + delete u.version + const stringified = `${JSON.stringify(u, 0, 2)}\n` + fs.writeFile(path.join('users', user), stringified, () => { }) + }) } if (typeof u.gravatar === 'string') { report(`Gravatar boolean encoded as string found in ${user}`, () => { - u.gravatar = u.gravatar === 'true'; - const stringified = `${JSON.stringify(u, 0, 2)}\n`; - fs.writeFile(path.join('users', user), stringified, () => { }); - }); + u.gravatar = u.gravatar === 'true' + const stringified = `${JSON.stringify(u, 0, 2)}\n` + fs.writeFile(path.join('users', user), stringified, () => { }) + }) } } catch ({ message }) { - report(`Invalid JSON in ${user} (${message})`); + report(`Invalid JSON in ${user} (${message})`) } } catch ({ message }) { - report(`Unable to read ${user} (${message})`); + report(`Unable to read ${user} (${message})`) } - }); + }) - const themes = await fs.readdir('themes'); + const themes = await fs.readdir('themes') await themes.forEach(async theme => { if (theme.endsWith('css')) { try { - const data = await fs.readFile(path.join('themes', theme), 'utf8'); + const data = await fs.readFile(path.join('themes', theme), 'utf8') try { - CSS.parse(data); + CSS.parse(data) } catch ({ message }) { - report(`Invalid CSS in ${theme} (${message})`); + report(`Invalid CSS in ${theme} (${message})`) } } catch ({ message }) { - report(`Unable to read ${theme} (${message})`); + report(`Unable to read ${theme} (${message})`) } } - }); -})(); + }) +})() diff --git a/users/volmering.json b/users/volmering.json index bca4a555..147fdc15 100644 --- a/users/volmering.json +++ b/users/volmering.json @@ -4,5 +4,5 @@ "format": "text", "license": "mit", "theme": "default", - "gravatar": "false" -} \ No newline at end of file + "gravatar": false +} diff --git a/yarn.lock b/yarn.lock index 43984aa3..46bd5c3c 100644 Binary files a/yarn.lock and b/yarn.lock differ