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) => {
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()
}

View file

@ -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()
}

View file

@ -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 holders>',
};
copyright: '<copyright holders>'
}
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()
}

View file

@ -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"

View file

@ -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 = `<a href="${stripTags(user.url)}">${name}</a>`;
html = `<a href="${stripTags(user.url)}">${name}</a>`
} else {
html = name;
html = name
}
if (user.email) {
html += ` &lt;<a href="mailto:${stripTags(user.email)}">${
plain ? user.email : escapeTags(user.email)
}</a>&gt;`;
}</a>&gt;`
}
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 = `<img id="gravatar" alt="Profile image" src="https://www.gravatar.com/avatar/${md5(
user.email.trim().toLowerCase()
)}" />`;
)}" />`
} else if (_.isObject(user.copyright[0]) && user.gravatar) {
// Supports multi-user format
gravatar = `<img id="gravatar" alt="Profile image" src="https://www.gravatar.com/avatar/${md5(
user.copyright[0].email.trim().toLowerCase()
)}" />`;
)}" />`
}
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>(.*)<\/article>/ms)[1];
const plain = content.match(/<article>(.*)<\/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 })
})
}

View file

@ -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'
)
}
};
}

View file

@ -1,16 +1,16 @@
const _ = require('lodash');
const _ = require('lodash')
const tags = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
};
const untags = _.invert(tags);
'&': '&amp;'
}
const untags = _.invert(tags)
module.exports = {
escapeTags: str => (str || '').replace(/[<>&]/g, m => tags[m]),
unescapeTags: str =>
(str || '').replace(/(&lt;|&gt;|&amp;)/g, m => untags[m]),
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
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}`)
})

66
test.js
View file

@ -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})`)
}
}
});
})();
})
})()

View file

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

BIN
yarn.lock

Binary file not shown.