-
Notifications
You must be signed in to change notification settings - Fork 7
/
index.js
206 lines (174 loc) · 6.11 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
const appDir = require('app-root-dir').get()
const fs = require('fs-extra')
const path = require('path')
const Prism = require('prismjs')
const prismPath = require.resolve('prismjs')
const prismStyleSheet = fs.readFileSync(path.join(prismPath.split('prism.js')[0], 'themes/prism.css'))
const tamper = require('tamper')
const template = require('es6-template-strings')
const { HtmlValidate } = require('html-validate')
const validatorErrorPage = fs.readFileSync(path.join(__dirname, 'templates/errorPage.html'))
module.exports = (app, params) => {
if (Object.prototype.hasOwnProperty.call(app, 'listen') || typeof app.listen === 'function') {
params = params || {} // two arguments
} else {
params = app // one argument
app = null
}
let render
if (app) {
render = app.response.render
}
let resModel
// sanitize config
let headerException = params.exceptions && params.exceptions.header ? params.exceptions.header : 'Partial'
headerException = headerException.toLowerCase()
const modelException = params.exceptions && params.exceptions.modelValue ? params.exceptions.modelValue : '_disableValidator'
let rules = params.validatorConfig && typeof params.validatorConfig === 'object' ? params.validatorConfig : {}
// default html-validate rules to use when none are passed
const defaultRules = {
extends: ['html-validate:standard']
}
/**
* Utility function to check if a route has any exceptions
*/
function validatorExceptions (req, res) {
// check the model
if (resModel) {
if (resModel[modelException]) {
resModel = undefined
return true
} else {
// clear out the cached model in both scenarios
resModel = undefined
}
}
// check headers
if (headerException) {
// check the request header
if (req.headers[headerException]) {
return true
}
// check the response header
if (res.getHeader(headerException)) {
return true
}
}
return false
}
// source validatorConfig
if (Object.keys(rules).length === 0) {
// when no config is passed check for a config file
const ruleFile = path.join(appDir, '.htmlValidate.json')
if (fileExists(ruleFile)) {
rules = require(ruleFile)
} else {
rules = defaultRules
}
}
// instantiate the validator module
const htmlValidate = new HtmlValidate(rules)
async function validate (body, res) {
// run the validator against the response body
const report = await htmlValidate.validateString(body)
if (!report.valid) {
const errorMap = new Map()
let parsedErrors = ''
for (const error of report.results[0].messages) {
const message = escapeHtmlEntities(error.message)
// first line is error message
parsedErrors += `${message}\n`
// next line is line and column numbers
parsedErrors += `At line ${error.line}, column ${error.column}\n\n`
// add error message and line number to map
errorMap.set(error.line, error.message)
}
const errorList = `<h2>Errors:</h2>\n<code class="validatorErrors">${parsedErrors}</code>`
// start building out stylized markup block
let formattedHTML = '<pre class=\'markup\'>\n<code class="language-html">\n'
const markupArray = body.split('\n')
// add line number highlighting for detected errors
for (let i = 0; i < markupArray.length; i++) {
const markupLine = markupArray[i]
if (errorMap.has(i + 1)) {
formattedHTML += `<span title='${errorMap.get(i + 1)}' class='line-numbers error'>`
formattedHTML += Prism.highlight(`${markupLine}`, Prism.languages.markup)
formattedHTML += '</span>'
} else {
formattedHTML += '<span class=\'line-numbers\'>'
formattedHTML += Prism.highlight(`${markupLine}`, Prism.languages.markup)
formattedHTML += '</span>'
}
}
// cap off the stylized markup blocks
formattedHTML += '</code>\n</pre>'
formattedHTML = `<h2>Markup used:</h2>\n${formattedHTML}`
// use 500 status for the validation error
if (res) {
res.status(500)
}
// build a model that includes error data, markup, and styling
const model = {
prismStyle: prismStyleSheet.toString(),
preWidth: markupArray.length.toString().length * 8,
errors: errorList,
markup: formattedHTML,
rawMarkup: body
}
// parse error page template and replace response body with it
body = template(validatorErrorPage, model)
}
return body
}
if (app) {
// use some method overload trickery to store a usable model reference
app.response.render = function (view, model, callback) {
// store a reference to the model if exceptions are being used and a model was set
if (model && typeof model === 'object') {
resModel = model
}
render.apply(this, arguments)
}
// validate responses under the right conditions
app.use(tamper((req, res) => {
/**
* Skip validation when:
* - HTTP status is not 200 (don't validate error pages)
* - content-type is not text/html (don't validate non-HTML responses)
* - No exception applies
*/
if (res.statusCode === 200 && res.getHeader('Content-Type') && res.getHeader('Content-Type').includes('text/html') && !validatorExceptions(req, res)) {
return (body) => {
return validate(body, res)
}
}
}))
}
return validate // export validate function for general use
}
/*
* Utility functions
*/
/**
* Escape special characters from HTML string
*
* @param {string} v - HTML string
* @returns {string} - New string with HTML entities escaped
*/
function escapeHtmlEntities (v) {
return v.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
}
/**
* Check if a file exists
*
* @param {string} path - Path to file to check
* @returns {boolean} - Whether or not that file exists
*/
function fileExists (path) {
try {
fs.accessSync(path)
return true
} catch {
return false
}
}