fix: Properly parse gravatar boolean and switch to standard linter.

Signed-off-by: Richie Bendall <richiebendall@gmail.com>
This commit is contained in:
Richie Bendall 2020-01-14 21:05:39 +13:00
parent dfcaa303de
commit f189d33f29
No known key found for this signature in database
GPG key ID: 1C6A99DFA9D306FC
11 changed files with 201 additions and 188 deletions

View file

@ -1,8 +1,8 @@
module.exports = (_req, res, next) => { module.exports = (_req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Origin', '*')
res.header( res.header(
'Access-Control-Allow-Headers', 'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept' 'Origin, X-Requested-With, Content-Type, Accept'
); )
next(); next()
}; }

View file

@ -1,63 +1,63 @@
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear()
module.exports = (req, res, next) => { module.exports = (req, res, next) => {
const parts = req.url.split('/'); const parts = req.url.split('/')
res.locals.options = parts.reduce( res.locals.options = parts.reduce(
(acc, curr) => { (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) { if (match.length) {
// Pinned year // Pinned year
if (curr.startsWith('@')) { if (curr.startsWith('@')) {
acc.pinnedYear = parseInt(curr.substr(1), 10); acc.pinnedYear = parseInt(curr.substr(1), 10)
} else { } 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) { if (match.length) {
acc.startYear = parseInt(match[1], 10); acc.startYear = parseInt(match[1], 10)
acc.endYear = parseInt(match[2], 10); acc.endYear = parseInt(match[2], 10)
return acc; return acc
} }
if (curr.startsWith('license')) { if (curr.startsWith('license')) {
acc.format = curr acc.format = curr
.split('.') .split('.')
.pop() .pop()
.trim(); .trim()
return acc; return acc
} }
if (curr.startsWith('+')) { if (curr.startsWith('+')) {
acc.license = curr.substr(1).toUpperCase(); acc.license = curr.substr(1).toUpperCase()
return acc; return acc
} }
acc.sha = curr; // not actually supported now - 2019-06-19 acc.sha = curr // not actually supported now - 2019-06-19
return acc; return acc
}, },
{ {
format: 'html', format: 'html',
startYear: null, startYear: null,
endYear: currentYear, endYear: currentYear,
sha: null, sha: null
} }
); )
if (res.locals.options.sha) { if (res.locals.options.sha) {
res.setHeader( res.setHeader(
'X-note', 'X-note',
'SHA and commit pinning is no longer supported, showing you latest release' 'SHA and commit pinning is no longer supported, showing you latest release'
); )
} }
next(); next()
}; }

View file

@ -1,35 +1,35 @@
const fs = require('fs-extra'); const fs = require('fs-extra')
const path = require('path'); const path = require('path')
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
const id = req.hostname.split('.')[0]; const id = req.hostname.split('.')[0]
res.locals.id = id; res.locals.id = id
if (req.method.toUpperCase() !== 'GET') { if (req.method.toUpperCase() !== 'GET') {
return next(); return next()
} }
// Otherwise load up the user json file // Otherwise load up the user json file
res.locals.user = { res.locals.user = {
copyright: '<copyright holders>', copyright: '<copyright holders>'
}; }
try { try {
const data = await fs.readFile( const data = await fs.readFile(
path.join(__dirname, '..', 'users', `${id}.json`), path.join(__dirname, '..', 'users', `${id}.json`),
'utf8' 'utf8'
); )
res.locals.user = { ...res.locals.user, ...JSON.parse(data) }; res.locals.user = { ...res.locals.user, ...JSON.parse(data) }
} catch ({ code, message }) { } catch ({ code, message }) {
if (code !== 'ENOENT') { if (code !== 'ENOENT') {
res res
.code(500) .code(500)
.send( .send(
`An internal error occurred - open an issue on https://github.com/remy/mit-license with the following information: ${message}` `An internal error occurred - open an issue on https://github.com/remy/mit-license with the following information: ${message}`
); )
return; return
} }
} }
next(); next()
}; }

View file

@ -23,7 +23,7 @@
"dev": "nodemon .", "dev": "nodemon .",
"serve": "node server.js", "serve": "node server.js",
"test": "node test.js", "test": "node test.js",
"lint": "eslint server.js middleware/*.js routes/*.js --color" "lint": "standard"
}, },
"bugs": { "bugs": {
"url": "https://github.com/remy/mit-license/issues" "url": "https://github.com/remy/mit-license/issues"
@ -41,17 +41,15 @@
"postcss-middleware": "^1.1.4", "postcss-middleware": "^1.1.4",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"serve-favicon": "^2.5.0", "serve-favicon": "^2.5.0",
"temp-dir": "^2.0.0" "temp-dir": "^2.0.0",
"yn": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@remy/eslint": "^3.2.2",
"babel-eslint": "^10.0.3",
"css": "^2.2.4", "css": "^2.2.4",
"eslint": "^6.8.0",
"eslint-plugin-node": "^11.0.0",
"has-flag": "^4.0.0", "has-flag": "^4.0.0",
"husky": "^3.1.0", "husky": "^4.0.7",
"nodemon": "^2.0.2" "nodemon": "^2.0.2",
"standard": "^14.3.1"
}, },
"resolutions": { "resolutions": {
"postcss-middleware/vinyl-fs/glob-stream/micromatch/braces": "^3.0.2" "postcss-middleware/vinyl-fs/glob-stream/micromatch/braces": "^3.0.2"

View file

@ -1,48 +1,48 @@
const md5 = require('md5'); const md5 = require('md5')
const path = require('path'); const path = require('path')
const { stripTags, escapeTags, unescapeTags } = require('./utils'); const { stripTags, escapeTags, unescapeTags } = require('./utils')
const _ = require('lodash'); const _ = require('lodash')
function getCopyrightHTML(user, plain) { function getCopyrightHTML (user, plain) {
let html = ''; let html = ''
const name = _.isString(user) const name = _.isString(user)
? user ? user
: plain : plain
? user.name || user.copyright ? user.name || user.copyright
: escapeTags(user.name || user.copyright); : escapeTags(user.name || user.copyright)
if (user.url) { if (user.url) {
html = `<a href="${stripTags(user.url)}">${name}</a>`; html = `<a href="${stripTags(user.url)}">${name}</a>`
} else { } else {
html = name; html = name
} }
if (user.email) { if (user.email) {
html += ` &lt;<a href="mailto:${stripTags(user.email)}">${ html += ` &lt;<a href="mailto:${stripTags(user.email)}">${
plain ? user.email : escapeTags(user.email) plain ? user.email : escapeTags(user.email)
}</a>&gt;`; }</a>&gt;`
} }
return html; return html
} }
module.exports = (req, res) => { module.exports = (req, res) => {
const { user, options } = res.locals; const { user, options } = res.locals
let name; let name
let gravatar; let gravatar
// No error and valid // No error and valid
if (user.copyright) { if (user.copyright) {
if (_.isString(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))) { } else if (_.isArray(user.copyright) && user.copyright.every(val => _.isString(val))) {
// Supports: ['Remy Sharp', 'Richie Bendall'] // Supports: ['Remy Sharp', 'Richie Bendall']
name = user.copyright name = user.copyright
.map(v => (options.format !== 'html' ? v : escapeTags(v))) .map(v => (options.format !== 'html' ? v : escapeTags(v)))
.join(', '); .join(', ')
} else { } else {
name = user.copyright.map(getCopyrightHTML).join(', '); name = user.copyright.map(getCopyrightHTML).join(', ')
} }
} }
@ -50,47 +50,47 @@ module.exports = (req, res) => {
// Supports regular format // Supports regular format
gravatar = `<img id="gravatar" alt="Profile image" src="https://www.gravatar.com/avatar/${md5( gravatar = `<img id="gravatar" alt="Profile image" src="https://www.gravatar.com/avatar/${md5(
user.email.trim().toLowerCase() user.email.trim().toLowerCase()
)}" />`; )}" />`
} else if (_.isObject(user.copyright[0]) && user.gravatar) { } else if (_.isObject(user.copyright[0]) && user.gravatar) {
// Supports multi-user format // Supports multi-user format
gravatar = `<img id="gravatar" alt="Profile image" src="https://www.gravatar.com/avatar/${md5( gravatar = `<img id="gravatar" alt="Profile image" src="https://www.gravatar.com/avatar/${md5(
user.copyright[0].email.trim().toLowerCase() user.copyright[0].email.trim().toLowerCase()
)}" />`; )}" />`
} }
const year = options.pinnedYear const year = options.pinnedYear
? options.pinnedYear ? options.pinnedYear
: [options.startYear, options.endYear].filter(Boolean).join('-'); : [options.startYear, options.endYear].filter(Boolean).join('-')
const license = (options.license || user.license || 'MIT').toUpperCase(); const license = (options.license || user.license || 'MIT').toUpperCase()
const format = options.format || user.format || 'html'; const format = options.format || user.format || 'html'
const args = { const args = {
info: `${year} ${name}`, info: `${year} ${name}`,
theme: user.theme || 'default', 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) => { req.app.render(filename, args, (error, content) => {
if (error) { if (error) {
res.status(500).send(error); res.status(500).send(error)
return; return
} }
if (format === 'txt') { if (format === 'txt') {
const plain = content.match(/<article>(.*)<\/article>/ms)[1]; const plain = content.match(/<article>(.*)<\/article>/ms)[1]
res res
.set('Content-Type', 'text/plain; charset=UTF-8') .set('Content-Type', 'text/plain; charset=UTF-8')
.send(unescapeTags(stripTags(plain)).trim()); .send(unescapeTags(stripTags(plain)).trim())
return; return
} }
if (format === 'html') { if (format === 'html') {
res.send(content); res.send(content)
return; return
} }
res.json({ ...user, ...options }); res.json({ ...user, ...options })
}); })
}; }

View file

@ -1,42 +1,44 @@
const fs = require('fs-extra'); const fs = require('fs-extra')
const path = require('path'); const path = require('path')
const btoa = require('btoa'); const btoa = require('btoa')
const { version } = require(path.join(__dirname, '..', 'package.json')); const { version } = require(path.join(__dirname, '..', 'package.json'))
const _ = require('lodash'); const _ = require('lodash')
const github = require('@octokit/rest')({ const github = require('@octokit/rest')({
// GitHub personal access token // GitHub personal access token
auth: process.env.github_token, auth: process.env.github_token,
// User agent with version from package.json // User agent with version from package.json
userAgent: `mit-license v${version}`, userAgent: `mit-license v${version}`
}); })
const { validDomainId } = require('./utils'); const yn = require('yn')
function getUserData({ query, body }) { const { validDomainId } = require('./utils')
function getUserData ({ query, body }) {
// If query parameters provided // If query parameters provided
if (_.size(query) > 0) return query; if (_.size(query) > 0) return query
// If the data parsed as {'{data: "value"}': ''} // 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 // Fallback
return body; return body
} }
// HTTP POST API // HTTP POST API
module.exports = async (req, res) => { 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']) // 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. // This includes the copyright, year, etc.
const userData = getUserData(req); const userData = getUserData(req)
// If there isn't enough part of the hostname // If there isn't enough part of the hostname
if (params.length < 2) { if (params.length < 2) {
res.status(400).send('Please specify a subdomain in the URL.'); res.status(400).send('Please specify a subdomain in the URL.')
return; return
} }
// Extract the name from the URL // Extract the name from the URL
const id = _.first(params); const id = _.first(params)
if (!validDomainId(id)) { if (!validDomainId(id)) {
// Return a vague error intentionally // Return a vague error intentionally
@ -44,31 +46,44 @@ module.exports = async (req, res) => {
.status(400) .status(400)
.send( .send(
'User already exists - to update values, please send a pull request on https://github.com/remy/mit-license' '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 // 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) { if (exists) {
res res
.status(409) .status(409)
.send( .send(
'User already exists - to update values, please send a pull request on https://github.com/remy/mit-license' '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 // File doesn't exist
// If copyright property and key doesn't exist // If copyright property and key doesn't exist
if (!userData.copyright) { if (!userData.copyright) {
res.status(400).send('JSON requires "copyright" property and value'); res.status(400).send('JSON requires "copyright" property and value')
return; return
} }
try { try {
const fileContent = JSON.stringify(userData, 0, 2); const fileContent = JSON.stringify(userData, 0, 2)
await github.repos.createOrUpdateFile({ await github.repos.createOrUpdateFile({
owner: 'remy', owner: 'remy',
@ -78,18 +93,18 @@ module.exports = async (req, res) => {
content: btoa(fileContent), content: btoa(fileContent),
committer: { committer: {
name: 'MIT License Bot', 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) { } catch (err) {
res res
.status(500) .status(500)
.send( .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'
); )
} }
}; }

View file

@ -1,16 +1,16 @@
const _ = require('lodash'); const _ = require('lodash')
const tags = { const tags = {
'<': '&lt;', '<': '&lt;',
'>': '&gt;', '>': '&gt;',
'&': '&amp;', '&': '&amp;'
}; }
const untags = _.invert(tags); const untags = _.invert(tags)
module.exports = { module.exports = {
escapeTags: str => (str || '').replace(/[<>&]/g, m => tags[m]), escapeTags: str => (str || '').replace(/[<>&]/g, m => tags[m]),
unescapeTags: str => unescapeTags: str =>
(str || '').replace(/(&lt;|&gt;|&amp;)/g, m => untags[m]), (str || '').replace(/(&lt;|&gt;|&amp;)/g, m => untags[m]),
stripTags: str => (str || '').replace(/<(?:.|\n)*?>/gm, ''), stripTags: str => (str || '').replace(/<(?:.|\n)*?>/gm, ''),
validDomainId: str => /^[\w-_]+$/.test(str), validDomainId: str => /^[\w-_]+$/.test(str)
}; }

View file

@ -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 Server port: The `PORT` environment variable can also be set to control the port the server
should be hosted on. should be hosted on.
*/ */
const express = require('express'); const express = require('express')
const minify = require('express-minify'); const minify = require('express-minify')
const favicon = require('serve-favicon'); const favicon = require('serve-favicon')
const postcssMiddleware = require('postcss-middleware'); const postcssMiddleware = require('postcss-middleware')
const tmpdir = require('temp-dir'); const tmpdir = require('temp-dir')
const path = require('path'); const path = require('path')
// Server // Server
var PORT = process.env.PORT || 8080; var PORT = process.env.PORT || 8080
// Prepare application // Prepare application
const app = express(); const app = express()
app.use( app.use(
minify({ minify({
cache: tmpdir, cache: tmpdir
}) })
); )
app.use(favicon(path.join(__dirname, 'favicon.ico'))); app.use(favicon(path.join(__dirname, 'favicon.ico')))
app.set('views', path.join(__dirname, '/licenses')); app.set('views', path.join(__dirname, '/licenses'))
app.set('view engine', 'ejs'); app.set('view engine', 'ejs')
// Setup static files // Setup static files
app.use('/robots.txt', express.static('robots.txt')); app.use('/robots.txt', express.static('robots.txt'))
app.use('/favicon.ico', express.static(`${__dirname}/favicon.ico`)); app.use('/favicon.ico', express.static(`${__dirname}/favicon.ico`))
app.use( app.use(
'/themes', '/themes',
postcssMiddleware({ postcssMiddleware({
plugins: [ plugins: [
require('postcss-preset-env')({ require('postcss-preset-env')({
overrideBrowserslist: '>= 0%', overrideBrowserslist: '>= 0%',
stage: 0, stage: 0
}), })
], ],
src(req) { src (req) {
return path.join(__dirname, 'themes', req.path); return path.join(__dirname, 'themes', req.path)
}, }
}), }),
express.static('themes') express.static('themes')
); )
// Middleware // Middleware
// CORS // CORS
app.use(require('./middleware/cors')); app.use(require('./middleware/cors'))
// Parse URL-encoded bodies (as sent by HTML forms) // Parse URL-encoded bodies (as sent by HTML forms)
app.use( app.use(
express.urlencoded({ express.urlencoded({
extended: true, extended: true
}) })
); )
// Parse JSON bodies (as sent by API clients) // Parse JSON bodies (as sent by API clients)
app.use(express.json()); app.use(express.json())
// Capture the id from the subdomain // Capture the id from the subdomain
app.use(require('./middleware/load-user')); app.use(require('./middleware/load-user'))
app.use(require('./middleware/load-options')); app.use(require('./middleware/load-options'))
// HTTP POST API // HTTP POST API
app.post('/', require('./routes/post')); app.post('/', require('./routes/post'))
app.get('/*', require('./routes/get')); app.get('/*', require('./routes/get'))
// Start listening for HTTP requests // Start listening for HTTP requests
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`🚀 on http://localhost:${PORT}`); console.log(`🚀 on http://localhost:${PORT}`)
}); })

66
test.js
View file

@ -1,68 +1,68 @@
const path = require('path'); const path = require('path')
const fs = require('fs-extra'); const fs = require('fs-extra')
const CSS = require('css'); const CSS = require('css')
const { validDomainId } = require('./routes/utils'); const { validDomainId } = require('./routes/utils')
const hasFlag = require('has-flag'); const hasFlag = require('has-flag')
function report(content, fix) { function report (content, fix) {
console.error(content); console.error(content)
if (fix && hasFlag('--fix')) fix(); if (fix && hasFlag('--fix')) fix()
process.exitCode = 1; process.exitCode = 1
} }
(async () => { (async () => {
const users = await fs.readdir('users'); const users = await fs.readdir('users')
users.forEach(async user => { users.forEach(async user => {
if (!user.endsWith('json')) { if (!user.endsWith('json')) {
report(`${user} is not a json file`, () => report(`${user} is not a json file`, () =>
fs.unlink(path.join('users', user), () => { }) fs.unlink(path.join('users', user), () => { })
); )
} }
if (!validDomainId(user.replace('.json', ''))) { if (!validDomainId(user.replace('.json', ''))) {
report(`${user} is not a valid domain id.`); report(`${user} is not a valid domain id.`)
} }
try { try {
const data = await fs.readFile(path.join('users', user), 'utf8'); const data = await fs.readFile(path.join('users', user), 'utf8')
try { try {
const u = JSON.parse(data); const u = JSON.parse(data)
if (!u.locked && !u.copyright) { if (!u.locked && !u.copyright) {
report(`Copyright not specified in ${user}`); report(`Copyright not specified in ${user}`)
} }
if (u.version) { if (u.version) {
report(`Version tag found in ${user}`, () => { report(`Version tag found in ${user}`, () => {
delete u.version; delete u.version
const stringified = `${JSON.stringify(u, 0, 2)}\n`; const stringified = `${JSON.stringify(u, 0, 2)}\n`
fs.writeFile(path.join('users', user), stringified, () => { }); fs.writeFile(path.join('users', user), stringified, () => { })
}); })
} }
if (typeof u.gravatar === 'string') { if (typeof u.gravatar === 'string') {
report(`Gravatar boolean encoded as string found in ${user}`, () => { report(`Gravatar boolean encoded as string found in ${user}`, () => {
u.gravatar = u.gravatar === 'true'; u.gravatar = u.gravatar === 'true'
const stringified = `${JSON.stringify(u, 0, 2)}\n`; const stringified = `${JSON.stringify(u, 0, 2)}\n`
fs.writeFile(path.join('users', user), stringified, () => { }); fs.writeFile(path.join('users', user), stringified, () => { })
}); })
} }
} catch ({ message }) { } catch ({ message }) {
report(`Invalid JSON in ${user} (${message})`); report(`Invalid JSON in ${user} (${message})`)
} }
} catch ({ 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 => { await themes.forEach(async theme => {
if (theme.endsWith('css')) { if (theme.endsWith('css')) {
try { try {
const data = await fs.readFile(path.join('themes', theme), 'utf8'); const data = await fs.readFile(path.join('themes', theme), 'utf8')
try { try {
CSS.parse(data); CSS.parse(data)
} catch ({ message }) { } catch ({ message }) {
report(`Invalid CSS in ${theme} (${message})`); report(`Invalid CSS in ${theme} (${message})`)
} }
} catch ({ message }) { } catch ({ message }) {
report(`Unable to read ${theme} (${message})`); report(`Unable to read ${theme} (${message})`)
} }
} }
}); })
})(); })()

View file

@ -4,5 +4,5 @@
"format": "text", "format": "text",
"license": "mit", "license": "mit",
"theme": "default", "theme": "default",
"gravatar": "false" "gravatar": false
} }

BIN
yarn.lock

Binary file not shown.