Skip to content

Commit 3eca2ec

Browse files
authored
Merge pull request #276 from kethinov/1.0.0
1.0.0
2 parents 73608c7 + a12f2a7 commit 3eca2ec

10 files changed

Lines changed: 623 additions & 845 deletions

File tree

.github/workflows/.ci.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,3 @@ jobs:
1616
- run: npm ci
1717
- run: npm run lint
1818
- run: npm run coverage
19-
- run: npm run codecov
20-
env:
21-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
- Put your changes here...
66

7+
## 1.0.0
8+
9+
- Added a new type of `exceptions` param called `routes`.
10+
- Refactored the code.
11+
- Updated various dependencies.
12+
713
## 0.2.5
814

915
- Fixed possible race condition.

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
If you are a maintainer of this module, please follow the following release procedure:
1212

13-
- Merge all desired pull requests into master.
13+
- Merge all desired pull requests into main.
1414
- Bump `package.json` to a new version and run `npm i` to generate a new `package-lock.json`.
1515
- Alter CHANGELOG "Next version" section and stamp it with the new version.
1616
- Paste contents of CHANGELOG into new version commit.

README.md

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# express-html-validator
22

33
[![Build Status](https://github.com/rooseveltframework/express-html-validator/workflows/CI/badge.svg
4-
)](https://github.com/rooseveltframework/express-html-validator/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/rooseveltframework/express-html-validator/branch/master/graph/badge.svg)](https://codecov.io/gh/rooseveltframework/express-html-validator) [![npm](https://img.shields.io/npm/v/express-html-validator.svg)](https://www.npmjs.com/package/express-html-validator)
4+
)](https://github.com/rooseveltframework/express-html-validator/actions?query=workflow%3ACI) [![npm](https://img.shields.io/npm/v/express-html-validator.svg)](https://www.npmjs.com/package/express-html-validator)
55

66
A [middleware](https://expressjs.com/en/guide/using-middleware.html) for the [Express framework](https://expressjs.com) that automatically validates the HTML on all your [Express routes](https://expressjs.com/en/guide/routing.html), powered by [html-validate](https://html-validate.org/). This module was built and is maintained by the [Roosevelt web framework](https://github.com/rooseveltframework/roosevelt) [team](https://github.com/orgs/rooseveltframework/people), but it can be used independently of Roosevelt as well.
77

@@ -11,8 +11,6 @@ First declare `express-html-validator` as a dependency in your app.
1111

1212
Then require the package into your application and call its constructor, passing along your Express app:
1313

14-
Usage with CommonJS:
15-
1614
```js
1715
const express = require('express')
1816
const expressValidator = require('express-html-validator')
@@ -31,30 +29,7 @@ app.get('/', (req, res) => {
3129
})
3230
```
3331

34-
Usage with ES modules:
35-
36-
```js
37-
import express from 'express';
38-
import expressValidator from 'express-html-validator'
39-
import { fileURLToPath } from 'url'
40-
import { dirname } from 'path'
41-
const router = express.Router();
42-
const app = express()
43-
const __filename = fileURLToPath(import.meta.url)
44-
const __dirname = dirname(__filename)
45-
46-
// Generally this would be used in development mode
47-
if (process.env.NODE_ENV === 'development') {
48-
expressValidator(app, config)
49-
}
50-
51-
// expressValidator should be called before defining routes
52-
router.get('/', (req, res) => {
53-
// This html response will be validated in real time as it's sent
54-
res.sendFile(__dirname + '/index.html');
55-
})
56-
```
57-
You can also run the validator on arbitrary strings outide of the Express context (example in CommonJS):
32+
You can also run the validator on arbitrary strings outside of the Express context:
5833

5934
```js
6035
const config = {}
@@ -84,6 +59,10 @@ Optionally you can pass this module a set of configs:
8459

8560
- `exceptions`: A set of params that can be used to prevent validation in certain scenarios:
8661

62+
- `routes` *[Array]*: An array of routes to exclude from validation. Supports wildcard `*` syntax.
63+
64+
- Default: `[]`.
65+
8766
- `header` *[String]*: A custom header that when set will disable the validator on a per request basis.
8867

8968
- Default: `'Partial'`.
@@ -105,4 +84,3 @@ Optionally you can pass this module a set of configs:
10584
"extends": ["html-validate:standard"]
10685
}
10786
```
108-

express-html-validator.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
const appDir = require('app-root-dir').get()
2+
const fs = require('fs')
3+
const path = require('path')
4+
const Prism = require('prismjs')
5+
const prismPath = require.resolve('prismjs')
6+
const prismStyleSheet = fs.readFileSync(path.join(prismPath.split('prism.js')[0], 'themes/prism.css'))
7+
const tamper = require('tamper')
8+
const { HtmlValidate } = require('html-validate')
9+
const validatorErrorPage = fs.readFileSync(path.join(__dirname, 'templates/errorPage.html'))
10+
11+
function templateLiteralRenderer (templateString, dataModel) {
12+
const templateFunction = new Function(...Object.keys(dataModel), `return \`${templateString}\`;`) // eslint-disable-line
13+
return templateFunction(...Object.values(dataModel))
14+
}
15+
16+
const { minimatch } = require('minimatch')
17+
function wildcardMatch (str, matchList) {
18+
for (let rule of matchList) {
19+
rule = path.normalize(rule).replace(/\\/g, '/') // normalize windows; including normalizing the slashes
20+
if (minimatch(str, rule)) return true
21+
}
22+
return false
23+
}
24+
25+
module.exports = (app, params) => {
26+
if (Object.prototype.hasOwnProperty.call(app, 'listen') || typeof app.listen === 'function') params = params || {} // two arguments
27+
else {
28+
params = app // one argument
29+
app = null
30+
}
31+
let render
32+
if (app) render = app.response.render
33+
let resModel
34+
const routeException = params?.exceptions?.routes || []
35+
let headerException = params?.exceptions?.header ? params.exceptions.header : 'Partial'
36+
headerException = headerException.toLowerCase()
37+
const modelException = params?.exceptions?.modelValue ? params.exceptions.modelValue : '_disableValidator'
38+
let rules = typeof params?.validatorConfig === 'object' ? params.validatorConfig : {}
39+
const defaultRules = { extends: ['html-validate:standard'] } // default html-validate rules to use when none are passed
40+
if (Object.keys(rules).length === 0) { // when no config is passed check for a config file
41+
const ruleFile = path.join(appDir, '.htmlValidate.json')
42+
if (fs.existsSync(ruleFile)) rules = require(ruleFile)
43+
else rules = defaultRules
44+
}
45+
const htmlValidate = new HtmlValidate(rules)
46+
47+
function reqExemptFromValidation (req, res) {
48+
// check for route exemptions
49+
if (wildcardMatch(req.route.path, routeException)) return true
50+
51+
// check for model exemptions
52+
if (resModel) {
53+
if (resModel[modelException]) {
54+
resModel = undefined
55+
return true
56+
} else resModel = undefined // clear out the cached model in both scenarios
57+
}
58+
59+
// check for head exemptions
60+
if (headerException) {
61+
if (req.headers[headerException]) return true // check the request header
62+
if (res.getHeader(headerException)) return true // check the response header
63+
}
64+
65+
return false
66+
}
67+
68+
async function validate (body, res) {
69+
const report = await htmlValidate.validateString(body) // run the validator against the response body
70+
if (!report.valid) {
71+
// the html failed validation
72+
const errorMap = new Map()
73+
let parsedErrors = ''
74+
for (const error of report.results[0].messages) {
75+
const message = error.message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&#34;').replace(/'/g, '&#39;') // escape html entities
76+
parsedErrors += `${message}\n` // first line is error message
77+
parsedErrors += `At line ${error.line}, column ${error.column}\n\n` // next line is line and column numbers
78+
errorMap.set(error.line, error.message) // add error message and line number to map
79+
}
80+
const errorList = `<h2>Errors:</h2>\n<code class="validatorErrors">${parsedErrors}</code>`
81+
82+
// start building out stylized markup block
83+
let formattedHTML = '<pre class=\'markup\'>\n<code class="language-html">\n'
84+
const markupArray = body.split('\n')
85+
86+
// add line number highlighting for detected errors
87+
for (let i = 0; i < markupArray.length; i++) {
88+
const markupLine = markupArray[i]
89+
if (errorMap.has(i + 1)) {
90+
formattedHTML += `<span title='${errorMap.get(i + 1)}' class='line-numbers error'>`
91+
formattedHTML += Prism.highlight(`${markupLine}`, Prism.languages.markup)
92+
formattedHTML += '</span>'
93+
} else {
94+
formattedHTML += '<span class=\'line-numbers\'>'
95+
formattedHTML += Prism.highlight(`${markupLine}`, Prism.languages.markup)
96+
formattedHTML += '</span>'
97+
}
98+
}
99+
100+
// cap off the stylized markup blocks
101+
formattedHTML += '</code>\n</pre>'
102+
formattedHTML = `<h2>Markup used:</h2>\n${formattedHTML}`
103+
104+
// use 500 status for the validation error
105+
if (res) res.status(500)
106+
107+
// build a model that includes error data, markup, and styling
108+
const model = {
109+
prismStyle: prismStyleSheet.toString(),
110+
preWidth: markupArray.length.toString().length * 8,
111+
errors: errorList,
112+
markup: formattedHTML,
113+
rawMarkup: body
114+
}
115+
116+
// parse error page template and replace response body with it
117+
body = templateLiteralRenderer(validatorErrorPage, model)
118+
}
119+
120+
return body
121+
}
122+
123+
if (app) {
124+
// use some method overload trickery to store a usable model reference
125+
app.response.render = function (view, model, callback) {
126+
if (model && typeof model === 'object') resModel = model // store a reference to the model if exceptions are being used and a model was set
127+
render.apply(this, arguments)
128+
}
129+
130+
// validate responses under the right conditions
131+
app.use(tamper((req, res) => {
132+
if (res.statusCode === 200 && res.getHeader?.('Content-Type')?.includes('text/html') && !reqExemptFromValidation(req, res)) return async (body) => await validate(body, res)
133+
}))
134+
}
135+
136+
return validate // export validate function for general use
137+
}

0 commit comments

Comments
 (0)