Signed-off-by: Richie Bendall <richiebendall@gmail.com>
This commit is contained in:
Richie Bendall 2020-11-07 11:56:03 +13:00
parent 6b54c63846
commit aa280a646b
No known key found for this signature in database
GPG key ID: 1C6A99DFA9D306FC
10 changed files with 158 additions and 159 deletions

View file

@ -1,5 +1,3 @@
# editorconfig.org
root = true
[*]

View file

@ -1,62 +1,42 @@
const currentYear = new Date().getFullYear()
module.exports = (req, res, next) => {
const parts = req.url.split('/')
const yearRegex = /^@?(\d{4})$/
const yearRangeRegex = /^(\d{4})-(\d{4})$/
res.locals.options = parts.reduce(
(acc, curr) => {
if (!curr) return acc
const getUrlParts = url => {
if (url === '/') {
return []
}
let match = curr.match(/^@?(\d{4})$/) || []
return url.slice(1).split('/')
}
if (match.length) {
// Pinned year
if (curr.startsWith('@')) {
acc.pinnedYear = parseInt(curr.substr(1), 10)
} else {
acc.startYear = parseInt(curr, 10)
}
return acc
module.exports = (request, response, next) => {
const urlParts = getUrlParts(request.url)
response.locals.options = {
format: 'html',
startYear: null,
endYear: currentYear
}
for (const urlPart of urlParts) {
if (yearRegex.test(urlPart)) {
if (urlPart.startsWith('@')) {
response.locals.options.pinnedYear = Number.parseInt(urlPart.slice(1))
} else {
response.locals.options.startYear = Number.parseInt(urlPart)
}
} else if (yearRangeRegex.test(urlPart)) {
const [startYear, endYear] = urlPart.match(yearRangeRegex).slice(1)
match = curr.match(/^(\d{4})-(\d{4})$/) || []
if (match.length) {
acc.startYear = parseInt(match[1], 10)
acc.endYear = parseInt(match[2], 10)
return acc
}
if (curr.startsWith('license')) {
acc.format = curr
.split('.')
.pop()
.trim()
return acc
}
if (curr.startsWith('+')) {
acc.license = curr.substr(1).toUpperCase()
return acc
}
acc.sha = curr // not actually supported now - 2019-06-19
return acc
},
{
format: 'html',
startYear: null,
endYear: currentYear,
sha: null
response.locals.options.startYear = Number.parseInt(startYear)
response.locals.options.endYear = Number.parseInt(endYear)
} else if (urlPart.startsWith('license')) {
response.locals.options.format = urlPart.split('.')[1].trim()
} else if (urlPart.startsWith('+')) {
response.locals.options.license = urlPart.slice(1).toUpperCase()
}
)
if (res.locals.options.sha) {
res.setHeader(
'X-note',
'SHA and commit pinning is no longer supported, showing you latest release'
)
}
next()

View file

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

View file

@ -26,16 +26,17 @@
"dependencies": {
"@octokit/rest": "^18.0.6",
"@sindresorhus/is": "^3.1.2",
"any-size": "^1.0.2",
"any-size": "^1.2.0",
"btoa": "^1.2.1",
"cors": "^2.8.5",
"create-html-element": "^3.0.0",
"ejs": "^3.1.5",
"escape-goat": "^3.0.0",
"express": "^4.17.1",
"express-minify": "^1.0.0",
"gravatar-url": "^3.1.0",
"html-text": "^1.0.1",
"load-json-file": "^6.2.0",
"md5": "^2.3.0",
"path-exists": "^4.0.0",
"postcss-middleware": "^1.1.4",
"postcss-preset-env": "^6.7.0",

View file

@ -1,97 +1,118 @@
const md5 = require('md5')
const path = require('path')
const { htmlEscape, htmlUnescape } = require('escape-goat')
const stripHtml = require('html-text')
const is = require('@sindresorhus/is')
const getGravatarUrl = require('gravatar-url')
const createHtmlElement = require('create-html-element')
const { renderFile } = require('ejs')
function getCopyrightHTML (user, plain) {
let html = ''
const name = is.string(user)
? user
: plain
? user.name || user.copyright
: htmlEscape(user.name || user.copyright)
if (user.url) {
html = `<a href="${stripHtml(user.url)}">${name}</a>`
} else {
html = name
const getCopyrightName = (user, isPlainText) => {
if (is.string(user)) {
return user
}
const copyright = user.name || user.copyright
return isPlainText ? copyright : htmlEscape(copyright)
}
const getCopyrightHtml = (user, isPlainText) => {
const name = getCopyrightName(user, isPlainText)
let html = user.url ? createHtmlElement({
name: 'a',
attributes: {
href: user.url
},
text: name
}) : name
if (user.email) {
html += ` &lt;<a href="mailto:${stripHtml(user.email)}">${
plain ? user.email : htmlEscape(user.email)
}</a>&gt;`
html += ` &lt;${createHtmlElement({
name: 'a',
attributes: {
href: `mailto:${user.email}`
},
text: user.email
})}&gt;`
}
return html
}
module.exports = (req, res) => {
const { user, options } = res.locals
const getGravatarEmail = user => {
if (user.gravatar && user.email) {
// Supports regular format
return user.email.trim().toLowerCase()
}
if (is.object(user.copyright[0]) && user.gravatar) {
// Supports multi-user format
return user.copyright[0].email.trim().toLowerCase()
}
}
const removeFalsy = array => array.filter(Boolean)
module.exports = async (_, response) => {
const { user, options } = response.locals
const isPlainText = options.format !== 'html'
let name
let gravatar
// No error and valid
if (user.copyright) {
if (is.string(user.copyright)) {
name = getCopyrightHTML(user, options.format !== 'html')
} else if (is.array(user.copyright) && user.copyright.every(val => is.string(val))) {
name = getCopyrightHtml(user, isPlainText)
} else if (is.array(user.copyright) && user.copyright.every(value => is.string(value))) {
// Supports: ['Remy Sharp', 'Richie Bendall']
name = user.copyright
.map(v => (options.format !== 'html' ? v : htmlEscape(v)))
.join(', ')
name = user.copyright.map(value => (isPlainText ? value : htmlEscape(value))).join(', ')
} else {
name = user.copyright.map(getCopyrightHTML).join(', ')
name = user.copyright.map(value => getCopyrightHtml(value)).join(', ')
}
}
if (user.gravatar && user.email) {
// Supports regular format
gravatar = `<img id="gravatar" alt="Profile image" src="https://www.gravatar.com/avatar/${md5(
user.email.trim().toLowerCase()
)}" />`
} else if (is.object(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()
)}" />`
let gravatar
const gravatarEmail = getGravatarEmail(user)
if (gravatarEmail) {
gravatar = createHtmlElement({
name: 'img',
attributes: {
id: 'gravatar',
alt: 'Profile image',
src: getGravatarUrl(gravatarEmail)
}
})
}
const year = options.pinnedYear
? options.pinnedYear
: [options.startYear, options.endYear].filter(Boolean).join('-')
: removeFalsy([options.startYear, options.endYear]).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
}
const filename = path.join(__dirname, '..', 'licenses', license)
req.app.render(filename, args, (error, content) => {
if (error) {
res.status(500).send(error)
return
}
try {
const content = await renderFile(path.join(__dirname, '..', 'licenses', `${license}.ejs`), {
info: `${year} ${name}`,
theme: user.theme || 'default',
gravatar
})
if (format === 'txt') {
const plain = content.match(/<article>(.*)<\/article>/ms)[1]
res
response
.set('Content-Type', 'text/plain; charset=UTF-8')
.send(htmlUnescape(stripHtml(plain)).trim())
return
}
if (format === 'html') {
res.send(content)
response.send(content)
return
}
res.json({ ...user, ...options })
})
response.json({ ...user, ...options })
} catch (error) {
response.status(500).send(error)
}
}

View file

@ -14,41 +14,45 @@ const github = new Octokit({
const yn = require('yn')
const is = require('@sindresorhus/is')
const { validDomainId } = require('./utils')
const { isDomainId } = 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 && !Object.values(body)[0]) return JSON.parse(Object.keys(body)[0])
if (size(body) === 1 && !Object.values(body)[0]) {
return JSON.parse(Object.keys(body)[0])
}
// Fallback
return body
}
// HTTP POST API
module.exports = async (req, res) => {
const { hostname } = req
module.exports = async (request, response) => {
const { hostname } = request
// Get different parts of hostname (example: remy.mit-license.org -> ['remy', 'mit-license', 'org'])
const params = hostname.split('.')
// This includes the copyright, year, etc.
const userData = getUserData(req)
const userData = getUserData(request)
// If there isn't enough part of the hostname
if (params.length < 2) {
res.status(400).send('Please specify a subdomain in the URL.')
response.status(400).send('Please specify a subdomain in the URL.')
return
}
// Extract the name from the URL
const id = params[0]
const [id] = params
if (!validDomainId(id)) {
if (!isDomainId(id)) {
// Return a vague error intentionally
res
response
.status(400)
.send(
'User already exists - to update values, please send a pull request on https://github.com/remy/mit-license'
@ -58,9 +62,8 @@ module.exports = async (req, res) => {
}
// Check if the user file exists in the users directory
const exists = await pathExists(path.join(__dirname, '..', 'users', `${id}.json`))
if (exists) {
res
if (await pathExists(path.join(__dirname, '..', 'users', `${id}.json`))) {
response
.status(409)
.send(
'User already exists - to update values, please send a pull request on https://github.com/remy/mit-license'
@ -72,7 +75,7 @@ module.exports = async (req, res) => {
// Parse the string version of a boolean or similar
userData.gravatar = yn(userData.gravatar, { lenient: true })
if (is.undefined(userData.gravatar)) {
res
response
.status(400)
.send(
'The "gravatar" JSON property must be a boolean.'
@ -84,28 +87,29 @@ module.exports = async (req, res) => {
// 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')
response.status(400).send('JSON requires "copyright" property and value')
return
}
try {
await github.repos.createOrUpdateFileContents({
owner: 'remy',
repo: 'mit-license',
path: `users/${id}.json`,
message: `Automated creation of user ${id}.`,
content: btoa(JSON.stringify(userData, 0, 2)),
committer: {
name: 'MIT License Bot',
email: 'remy@leftlogic.com'
}
})
await Promise.all([
github.repos.createOrUpdateFileContents({
owner: 'remy',
repo: 'mit-license',
path: `users/${id}.json`,
message: `Automated creation of user ${id}.`,
content: btoa(JSON.stringify(userData, 0, 2)),
committer: {
name: 'MIT License Bot',
email: 'remy@leftlogic.com'
}
}),
writeJsonFile(path.join(__dirname, '..', 'users', `${id}.json`), userData, { indent: undefined })
])
await writeJsonFile(path.join(__dirname, '..', 'users', `${id}.json`), userData, { indent: undefined })
res.status(201).send(`MIT license page created: https://${hostname}`)
} catch (err) {
res
response.status(201).send(`MIT license page created: https://${hostname}`)
} catch {
response
.status(500)
.send(
'Unable to create new user - please send a pull request on https://github.com/remy/mit-license'

View file

@ -1,3 +1 @@
module.exports = {
validDomainId: str => /^[\w-_]+$/.test(str)
}
exports.isDomainId = value => /^[\w-_]+$/.test(value)

View file

@ -24,7 +24,6 @@ app.use(
)
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'))
@ -34,12 +33,11 @@ app.use(
postcssMiddleware({
plugins: [
require('postcss-preset-env')({
overrideBrowserslist: '>= 0%',
stage: 0
overrideBrowserslist: '>= 0%'
})
],
src (req) {
return path.join(__dirname, 'themes', req.path)
src (request) {
return path.join(__dirname, 'themes', request.path)
}
}),
express.static('themes')

View file

@ -1,7 +1,7 @@
const { promises: fs } = require('fs')
const writeJsonFile = require('write-json-file')
const CSS = require('css')
const { validDomainId } = require('./routes/utils')
const { isDomainId } = require('./routes/utils')
const hasFlag = require('has-flag')
const getExtension = require('file-ext')
const path = require('path-extra')
@ -23,7 +23,7 @@ async function report (content, fix) {
})
}
if (!validDomainId(path.base(user))) {
if (!isDomainId(path.base(user))) {
await report(`${user} is not a valid domain id.`)
}

BIN
yarn.lock

Binary file not shown.