diff --git a/services/w3c/w3c-validation-helper.js b/services/w3c/w3c-validation-helper.js new file mode 100644 index 0000000000000000000000000000000000000000..3f0a75f4016db4f23f8aefa335cb01b790d33ed0 --- /dev/null +++ b/services/w3c/w3c-validation-helper.js @@ -0,0 +1,156 @@ +'use strict' + +const html5Expression = + '^HTML\\s?,\\s?SVG\\s?1\\.1\\s?,\\s?MathML\\s?3\\.0(\\s?,\\s?((ITS\\s?2\\.0)|(RDFa\\s?Lite\\s?1\\.1)))?$' +const html4Expression = + '^HTML\\s?4\\.01\\s?(Strict|Transitional|Frameset)\\s?,\\s?URL\\s?\\/\\s?XHTML\\s?1\\.0\\s?(Strict|Transitional|Frameset)\\s?,\\s?URL$' +const xhtmlExpression = + '^(XHTML\\s?,\\s?SVG\\s?1\\.1\\s?,\\s?MathML\\s?3\\.0(\\s?,\\s?RDFa\\s?Lite\\s?1\\.1)?)|(XHTML\\s?1\\.0\\s?Strict\\s?,\\s?URL\\s?,\\s?Ruby\\s?,\\s?SVG\\s?1\\.1\\s?,\\s?MathML\\s?3\\.0)$' +const svgExpression = + '^SVG\\s?1\\.1\\s?,\\s?URL\\s?,\\s?XHTML\\s?,\\s?MathML\\s?3\\.0$' +const presetRegex = new RegExp( + `(${html5Expression})|(${html4Expression})|(${xhtmlExpression})|(${svgExpression})`, + 'i' +) + +const getMessage = messageTypes => { + const messageTypeKeys = Object.keys(messageTypes) + messageTypeKeys.sort() // Sort to make the order error, warning for display + + if (messageTypeKeys.length === 0) { + return 'validated' + } + + const messages = messageTypeKeys.map( + key => `${messageTypes[key]} ${key}${messageTypes[key] > 1 ? 's' : ''}` + ) + return messages.join(', ') +} + +const getColor = messageTypes => { + if ('error' in messageTypes) { + return 'red' + } + + if ('warning' in messageTypes) { + return 'yellow' + } + + return 'brightgreen' +} + +const getSchema = preset => { + if (!preset) return undefined + const decodedPreset = decodeURI(preset) + const schema = [] + if (new RegExp(html4Expression, 'i').test(decodedPreset)) { + if (/Strict/i.test(decodedPreset)) { + schema.push('http://s.validator.nu/xhtml10/xhtml-strict.rnc') + } else if (/Transitional/i.test(decodedPreset)) { + schema.push('http://s.validator.nu/xhtml10/xhtml-transitional.rnc') + } else { + schema.push('http://s.validator.nu/xhtml10/xhtml-frameset.rnc') + } + schema.push('http://c.validator.nu/all-html4/') + } else if (/1\.0 Strict, URL, Ruby, SVG 1\.1/i.test(decodedPreset)) { + schema.push('http://s.validator.nu/xhtml1-ruby-rdf-svg-mathml.rnc') + schema.push('http://c.validator.nu/all-html4/') + } else { + if (new RegExp(html5Expression, 'i').test(decodedPreset)) { + if (/ITS 2\.0/i.test(decodedPreset)) { + schema.push('http://s.validator.nu/html5-its.rnc') + } else if (/RDFa Lite 1\.1/i.test(decodedPreset)) { + schema.push('http://s.validator.nu/html5-rdfalite.rnc') + } else { + schema.push('http://s.validator.nu/html5.rnc') + } + } else if (new RegExp(xhtmlExpression, 'i').test(decodedPreset)) { + if (/RDFa Lite 1\.1/i.test(decodedPreset)) { + schema.push('http://s.validator.nu/xhtml5-rdfalite.rnc') + } else { + schema.push('http://s.validator.nu/xhtml5.rnc') + } + } else if (new RegExp(svgExpression, 'i').test(decodedPreset)) { + schema.push('http://s.validator.nu/svg-xhtml5-rdf-mathml.rnc') + } + schema.push('http://s.validator.nu/html5/assertions.sch') + schema.push('http://c.validator.nu/all/') + } + return schema.map(url => encodeURI(url)).join(' ') +} + +const documentation = ` + <style> + .box { + display: flex; + justify-content: space-between; + } + .note { + font-size: smaller; + text-align: left; + } + </style> + <p> + The W3C validation badge performs validation of the HTML, SVG, MathML, ITS, RDFa Lite, XHTML documents. + The badge uses the type property of each message found in the messages from the validation results to determine to be an error or warning. + The rules are as follows: + <ul class="note"> + <li>info: These messages are counted as warnings</li> + <li>error: These messages are counted as errors</li> + <li>non-document-error: These messages are counted as errors</li> + </ul> + </p> + <p> + This badge relies on the https://validator.nu/ service to perform the validation. Please refer to https://about.validator.nu/ for the full documentation and Terms of service. + The following are required from the consumer for the badge to function. + + <ul class="note"> + <li> + Path: + <ul> + <li> + parser: The parser that is used for validation. This is a passthru value to the service + <ul> + <li>default <i>(This will not pass a parser to the API and make the API choose the parser based on the validated content)</i></li> + <li>html <i>(HTML)</i></li> + <li>xml <i>(XML; don’t load external entities)</i></li> + <li>xmldtd <i>(XML; load external entities)</i></li> + </ul> + </li> + </ul> + </li> + <li> + Query string: + <ul> + <li> + targetUrl (Required): This is the path for the document to be validated + </li> + <li> + preset (Optional can be left as blank): This is used to determine the schema for the document to be valdiated against. + The following are the allowed values + <ul> + <li>HTML, SVG 1.1, MathML 3.0</li> + <li>HTML, SVG 1.1, MathML 3.0, ITS 2.0</li> + <li>HTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1</li> + <li>HTML 4.01 Strict, URL / XHTML 1.0 Strict, URL</li> + <li>HTML 4.01 Transitional, URL / XHTML 1.0 Transitional, URL</li> + <li>HTML 4.01 Frameset, URL / XHTML 1.0 Frameset, URL</li> + <li>XHTML, SVG 1.1, MathML 3.0</li> + <li>XHTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1</li> + <li>XHTML 1.0 Strict, URL, Ruby, SVG 1.1, MathML 3.0</li> + <li>SVG 1.1, URL, XHTML, MathML 3.0</li> + </ul> + </li> + </ul> + </li> + </ul> + </p> +` + +module.exports = { + documentation, + presetRegex, + getColor, + getMessage, + getSchema, +} diff --git a/services/w3c/w3c-validation-helper.spec.js b/services/w3c/w3c-validation-helper.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b5c98ed85283ff9afce8c9da7f0bbacc74c7ab61 --- /dev/null +++ b/services/w3c/w3c-validation-helper.spec.js @@ -0,0 +1,265 @@ +'use strict' +const { expect } = require('chai') +const { test, given, forCases } = require('sazerac') +const { + presetRegex, + getMessage, + getColor, + getSchema, +} = require('./w3c-validation-helper') + +describe('w3c-validation-helper', function() { + describe('presetRegex', function() { + function testing(preset) { + return presetRegex.test(preset) + } + + test(testing, () => { + forCases([ + given('html,svg 1.1,mathml 3.0'), + given('HTML,SVG 1.1,MathML 3.0'), + given('HTML, SVG 1.1, MathML 3.0'), + given('HTML , SVG 1.1 , MathML 3.0'), + given('HTML,SVG 1.1,MathML 3.0,ITS 2.0'), + given('HTML, SVG 1.1, MathML 3.0, ITS 2.0'), + given('HTML , SVG 1.1 , MathML 3.0 , ITS 2.0'), + given('HTML,SVG 1.1,MathML 3.0,RDFa Lite 1.1'), + given('HTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1'), + given('HTML , SVG 1.1 , MathML 3.0 , RDFa Lite 1.1'), + given('HTML 4.01 Strict,URL/XHTML 1.0 Strict,URL'), + given('HTML 4.01 Strict, URL/ XHTML 1.0 Strict, URL'), + given('HTML 4.01 Strict , URL / XHTML 1.0 Strict , URL'), + given('HTML 4.01 Transitional,URL/XHTML 1.0 Transitional,URL'), + given('HTML 4.01 Transitional, URL/ XHTML 1.0 Transitional, URL'), + given('HTML 4.01 Transitional , URL / XHTML 1.0 Transitional , URL'), + given('HTML 4.01 Frameset,URL/XHTML 1.0 Frameset,URL'), + given('HTML 4.01 Frameset, URL/ XHTML 1.0 Frameset, URL'), + given('HTML 4.01 Frameset , URL / XHTML 1.0 Frameset , URL'), + given('XHTML,SVG 1.1,MathML 3.0'), + given('XHTML, SVG 1.1, MathML 3.0'), + given('XHTML , SVG 1.1 , MathML 3.0'), + given('XHTML,SVG 1.1,MathML 3.0,RDFa Lite 1.1'), + given('XHTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1'), + given('XHTML , SVG 1.1 , MathML 3.0 , RDFa Lite 1.1'), + given('XHTML 1.0 Strict,URL,Ruby,SVG 1.1,MathML 3.0'), + given('XHTML 1.0 Strict, URL, Ruby, SVG 1.1, MathML 3.0'), + given('XHTML 1.0 Strict , URL , Ruby , SVG 1.1 , MathML 3.0'), + given('SVG 1.1,URL,XHTML,MathML 3.0'), + given('SVG 1.1, URL, XHTML, MathML 3.0'), + given('SVG 1.1 , URL , XHTML , MathML 3.0'), + ]).expect(true) + }) + + test(testing, () => { + forCases([ + given(undefined), + given(null), + given(''), + given(' '), + given('HTML'), + ]).expect(false) + }) + }) + + describe('getColor', function() { + it('returns "brightgreen" if no messages are provided', function() { + const messageTypes = {} + + const actualResult = getColor(messageTypes) + + expect(actualResult).to.equal('brightgreen') + }) + + it('returns "yellow" if only warning messages are provided', function() { + const messageTypes = { warning: 1 } + + const actualResult = getColor(messageTypes) + + expect(actualResult).to.equal('yellow') + }) + + it('returns "red" if only error messages are provided', function() { + const messageTypes = { error: 1 } + + const actualResult = getColor(messageTypes) + + expect(actualResult).to.equal('red') + }) + + it('returns "red" if both warning and error messages are provided', function() { + const messageTypes = { warning: 3, error: 4 } + + const actualResult = getColor(messageTypes) + + expect(actualResult).to.equal('red') + }) + }) + + describe('getMessage', function() { + it('returns "validate" if no messages are provided', function() { + const messageTypes = {} + + const actualResult = getMessage(messageTypes) + + expect(actualResult).to.equal('validated') + }) + + it('returns "1 error" if 1 error message is provided', function() { + const messageTypes = { error: 1 } + + const actualResult = getMessage(messageTypes) + + expect(actualResult).to.equal('1 error') + }) + + it('returns "2 errors" if 2 error messages are provided', function() { + const messageTypes = { error: 2 } + + const actualResult = getMessage(messageTypes) + + expect(actualResult).to.equal('2 errors') + }) + + it('returns "1 warning" if 1 warning message is provided', function() { + const messageTypes = { warning: 1 } + + const actualResult = getMessage(messageTypes) + + expect(actualResult).to.equal('1 warning') + }) + + it('returns "2 warnings" if 2 warning messages are provided', function() { + const messageTypes = { warning: 2 } + + const actualResult = getMessage(messageTypes) + + expect(actualResult).to.equal('2 warnings') + }) + + it('returns "1 error, 1 warning" if 1 error and 1 warning message is provided', function() { + const messageTypes = { warning: 1, error: 1 } + + const actualResult = getMessage(messageTypes) + + expect(actualResult).to.equal('1 error, 1 warning') + }) + + it('returns "2 errors, 2 warnings" if 2 error and 2 warning message is provided', function() { + const messageTypes = { error: 2, warning: 2 } + + const actualResult = getMessage(messageTypes) + + expect(actualResult).to.equal('2 errors, 2 warnings') + }) + }) + + describe('getSchema', function() { + function execution(preset) { + return getSchema(preset) + } + + test(execution, () => { + forCases([given(undefined), given(null), given('')]).expect(undefined) + }) + + it('returns 3 schemas associated to the "HTML,SVG 1.1,MathML 3.0" preset', function() { + const preset = 'HTML,SVG 1.1,MathML 3.0' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/html5.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/' + ) + }) + + it('returns 3 schemas associated to the "HTML,SVG 1.1,MathML 3.0,ITS 2.0" preset', function() { + const preset = 'HTML,SVG 1.1,MathML 3.0,ITS 2.0' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/html5-its.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/' + ) + }) + + it('returns 3 schemas associated to the "HTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1" preset', function() { + const preset = 'HTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/html5-rdfalite.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/' + ) + }) + + it('returns 3 schemas associated to the "HTML 4.01 Strict, URL/ XHTML 1.0 Strict, URL" preset', function() { + const preset = 'HTML 4.01 Strict, URL/ XHTML 1.0 Strict, URL' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/xhtml10/xhtml-strict.rnc http://c.validator.nu/all-html4/' + ) + }) + + it('returns 3 schemas associated to the "HTML 4.01 Transitional, URL/ XHTML 1.0 Transitional, URL" preset', function() { + const preset = 'HTML 4.01 Transitional, URL/ XHTML 1.0 Transitional, URL' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/xhtml10/xhtml-transitional.rnc http://c.validator.nu/all-html4/' + ) + }) + + it('returns 3 schemas associated to the "HTML 4.01 Frameset, URL/ XHTML 1.0 Frameset, URL" preset', function() { + const preset = 'HTML 4.01 Frameset, URL/ XHTML 1.0 Frameset, URL' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/xhtml10/xhtml-frameset.rnc http://c.validator.nu/all-html4/' + ) + }) + + it('returns 3 schemas associated to the "XHTML, SVG 1.1, MathML 3.0" preset', function() { + const preset = 'XHTML, SVG 1.1, MathML 3.0' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/xhtml5.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/' + ) + }) + + it('returns 3 schemas associated to the "XHTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1" preset', function() { + const preset = 'XHTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/xhtml5-rdfalite.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/' + ) + }) + + it('returns 3 schemas associated to the "XHTML 1.0 Strict, URL, Ruby, SVG 1.1, MathML 3.0" preset', function() { + const preset = 'XHTML 1.0 Strict, URL, Ruby, SVG 1.1, MathML 3.0' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/xhtml1-ruby-rdf-svg-mathml.rnc http://c.validator.nu/all-html4/' + ) + }) + + it('returns 3 schemas associated to the "SVG 1.1, URL, XHTML, MathML 3.0" preset', function() { + const preset = 'SVG 1.1, URL, XHTML, MathML 3.0' + + const actualResult = getSchema(preset) + + expect(actualResult).to.equal( + 'http://s.validator.nu/svg-xhtml5-rdf-mathml.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/' + ) + }) + }) +}) diff --git a/services/w3c/w3c-validation.service.js b/services/w3c/w3c-validation.service.js new file mode 100644 index 0000000000000000000000000000000000000000..474f98295628e9f08972fcea6cc53d5fefa73be1 --- /dev/null +++ b/services/w3c/w3c-validation.service.js @@ -0,0 +1,136 @@ +'use strict' +const Joi = require('@hapi/joi') +const { optionalUrl } = require('../validators') +const { + documentation, + presetRegex, + getColor, + getMessage, + getSchema, +} = require('./w3c-validation-helper') +const { BaseJsonService, NotFound } = require('..') + +const schema = Joi.object({ + url: Joi.string().optional(), + messages: Joi.array() + .required() + .items( + Joi.object({ + type: Joi.string() + .allow('info', 'error', 'non-document-error') + .required(), + subType: Joi.string().optional(), + message: Joi.string().required(), + }) + ), +}).required() + +const queryParamSchema = Joi.object({ + targetUrl: optionalUrl.required(), + preset: Joi.string() + .regex(presetRegex) + .allow(''), +}).required() + +module.exports = class W3cValidation extends BaseJsonService { + static get category() { + return 'analysis' + } + + static get route() { + return { + base: 'w3c-validation', + pattern: ':parser(default|html|xml|xmldtd)', + queryParamSchema, + } + } + + static get examples() { + return [ + { + title: 'W3C Validation', + namedParams: { parser: 'html' }, + queryParams: { + targetUrl: 'https://validator.nu/', + preset: 'HTML, SVG 1.1, MathML 3.0', + }, + staticPreview: this.render({ messageTypes: {} }), + documentation, + }, + ] + } + + static get defaultBadgeData() { + return { + label: 'w3c', + } + } + + static render({ messageTypes }) { + return { + message: getMessage(messageTypes), + color: getColor(messageTypes), + } + } + + async fetch(targetUrl, preset, parser) { + return this._requestJson({ + url: 'https://validator.nu/', + schema, + options: { + qs: { + schema: getSchema(preset), + parser: parser === 'default' ? undefined : parser, + doc: encodeURI(targetUrl), + out: 'json', + }, + }, + }) + } + + transform(url, messages) { + if (messages.length === 1) { + const { subType, type, message } = messages[0] + if (type === 'non-document-error' && subType === 'io') { + let notFound = false + if ( + message === + 'HTTP resource not retrievable. The HTTP status from the remote server was: 404.' + ) { + notFound = true + } else if (message.endsWith('Name or service not known')) { + const domain = message.split(':')[0].trim() + notFound = url.indexOf(domain) !== -1 + } + + if (notFound) { + throw new NotFound({ prettyMessage: 'target url not found' }) + } + } + } + + return messages.reduce((accumulator, message) => { + let { type } = message + if (type === 'info') { + type = 'warning' + } else { + // All messages are suppose to have a type and there can only be info, error or non-document + // If a new type gets introduce this will flag them as errors + type = 'error' + } + + if (!(type in accumulator)) { + accumulator[type] = 0 + } + accumulator[type] += 1 + return accumulator + }, {}) + } + + async handle({ parser }, { targetUrl, preset }) { + const { url, messages } = await this.fetch(targetUrl, preset, parser) + return this.constructor.render({ + messageTypes: this.transform(url, messages), + }) + } +} diff --git a/services/w3c/w3c-validation.tester.js b/services/w3c/w3c-validation.tester.js new file mode 100644 index 0000000000000000000000000000000000000000..b2eff4457c3b4019c4f522e195948b535c08ab40 --- /dev/null +++ b/services/w3c/w3c-validation.tester.js @@ -0,0 +1,100 @@ +'use strict' +const Joi = require('@hapi/joi') +const t = (module.exports = require('../tester').createServiceTester()) + +const isErrorOnly = Joi.string().regex(/^[0-9]+ errors?$/) + +const isWarningOnly = Joi.string().regex(/^[0-9]+ warnings?$/) + +const isErrorAndWarning = Joi.string().regex( + /^[0-9]+ errors?, [0-9]+ warnings?$/ +) + +const isW3CMessage = Joi.alternatives().try( + 'validated', + isErrorOnly, + isWarningOnly, + isErrorAndWarning +) +const isW3CColors = Joi.alternatives().try('brightgreen', 'red', 'yellow') +t.create( + 'W3C Validation page conforms to standards with no preset and parser with brightgreen badge' +) + .get( + '/default.json?targetUrl=https://hsivonen.com/test/moz/messages-types/no-message.html' + ) + .expectBadge({ + label: 'w3c', + message: isW3CMessage, + color: isW3CColors, + }) + +t.create( + 'W3C Validation page conforms to standards with no HTML4 preset and HTML parser with brightgreen badge' +) + .get( + '/html.json?targetUrl=https://hsivonen.com/test/moz/messages-types/no-message.html&preset=HTML,%20SVG%201.1,%20MathML%203.0' + ) + .expectBadge({ + label: 'w3c', + message: isW3CMessage, + color: isW3CColors, + }) + +t.create('W3C Validation target url not found error') + .get( + '/default.json?targetUrl=http://hsivonen.com/test/moz/messages-types/404.html' + ) + .expectBadge({ + label: 'w3c', + message: 'target url not found', + }) + +t.create('W3C Validation target url host not found error') + .get('/default.json?targetUrl=https://adfasdfasdfasdfadfadfadfasdfadf.com') + .expectBadge({ + label: 'w3c', + message: 'target url not found', + }) + +t.create('W3C Validation page has 1 validation error with red badge') + .get( + '/default.json?targetUrl=http://hsivonen.com/test/moz/messages-types/warning.html' + ) + .expectBadge({ + label: 'w3c', + message: isW3CMessage, + color: isW3CColors, + }) + +t.create( + 'W3C Validation page has 3 validation error using HTML 4.01 Frameset preset with red badge' +) + .get( + '/html.json?targetUrl=http://hsivonen.com/test/moz/messages-types/warning.html&preset=HTML 4.01 Frameset, URL / XHTML 1.0 Frameset, URL' + ) + .expectBadge({ + label: 'w3c', + message: isW3CMessage, + color: isW3CColors, + }) + +t.create('W3C Validation page has 1 validation warning with yellow badge') + .get( + '/default.json?targetUrl=http://hsivonen.com/test/moz/messages-types/info.svg' + ) + .expectBadge({ + label: 'w3c', + message: isW3CMessage, + color: isW3CColors, + }) + +t.create('W3C Validation page has multiple of validation errors with red badge') + .get( + '/default.json?targetUrl=http://hsivonen.com/test/moz/messages-types/range-error.html' + ) + .expectBadge({ + label: 'w3c', + message: isW3CMessage, + color: isW3CColors, + })