-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
feat(isXsdAnyURI): add XML Schema anyURI validator #2636
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MazenSamehR
wants to merge
2
commits into
validatorjs:master
Choose a base branch
from
MazenSamehR:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+366
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,300 @@ | ||
| import assertString from './util/assertString'; | ||
| import isIP from './isIP'; | ||
|
|
||
| const MULTIPLE_SPACES_REGEX = / {2,}/g; | ||
| const INVALID_PERCENT_REGEX = /%(?![0-9A-Fa-f]{2})/; | ||
| const SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*$/; | ||
| const BACKSLASH_REGEX = /\\/; | ||
| const DISALLOWED_ASCII_REGEX = /["<>^`{}|]/; | ||
| const OPEN_BRACKET_PLACEHOLDER = '__VALIDATOR_OPEN_BRACKET__'; | ||
| const CLOSE_BRACKET_PLACEHOLDER = '__VALIDATOR_CLOSE_BRACKET__'; | ||
|
|
||
| const HEX_DIGIT = '[0-9A-Fa-f]'; | ||
| const PCT_ENCODED = `%${HEX_DIGIT}{2}`; | ||
| const UNRESERVED = 'A-Za-z0-9\\-._~'; | ||
| const SUB_DELIMS = "!$&'()*+,;="; | ||
| const PCHAR = `(?:[${UNRESERVED}]|${PCT_ENCODED}|[${SUB_DELIMS}:@])`; | ||
| const SEGMENT = `(?:${PCHAR})*`; | ||
| const SEGMENT_NZ = `(?:${PCHAR})+`; | ||
| const SEGMENT_NZ_NC = `(?:${PCT_ENCODED}|[${UNRESERVED}${SUB_DELIMS}@])+`; | ||
|
|
||
| const PATH_ABEMPTY_REGEX = new RegExp(`^(?:/${SEGMENT})*$`); | ||
| const PATH_ABSOLUTE_REGEX = new RegExp(`^/(?:${SEGMENT_NZ}(?:/${SEGMENT})*)?$`); | ||
| const PATH_ROOTLESS_REGEX = new RegExp(`^${SEGMENT_NZ}(?:/${SEGMENT})*$`); | ||
| const PATH_NOSCHEME_REGEX = new RegExp(`^(?:${SEGMENT_NZ_NC})(?:/${SEGMENT})*$`); | ||
| const QUERY_FRAGMENT_REGEX = new RegExp(`^(?:${PCHAR}|[/?])*$`); | ||
| const USERINFO_REGEX = new RegExp(`^(?:${PCT_ENCODED}|[${UNRESERVED}${SUB_DELIMS}:])*$`); | ||
| const REG_NAME_REGEX = new RegExp(`^(?:${PCT_ENCODED}|[${UNRESERVED}${SUB_DELIMS}])*$`); | ||
| const IPV_FUTURE_REGEX = /^v[0-9A-F]+\.[A-Za-z0-9._~!$&'()*+,;=:-]+$/i; | ||
|
|
||
| function collapseXmlWhitespace(input) { | ||
| let normalized = ''; | ||
|
|
||
| for (let i = 0; i < input.length; i += 1) { | ||
| const code = input.charCodeAt(i); | ||
|
|
||
| if (code === 0x09 || code === 0x0a || code === 0x0d) { | ||
| normalized += ' '; | ||
| } else { | ||
| normalized += input[i]; | ||
| } | ||
| } | ||
|
|
||
| return normalized.replace(MULTIPLE_SPACES_REGEX, ' ').trim(); | ||
| } | ||
|
|
||
| function containsForbiddenControl(value) { | ||
| for (let i = 0; i < value.length; i += 1) { | ||
| const code = value.charCodeAt(i); | ||
|
|
||
| if ( | ||
| (code >= 0x00 && code <= 0x08) || | ||
| code === 0x0b || | ||
| code === 0x0c || | ||
| (code >= 0x0e && code <= 0x1f) || | ||
| code === 0x7f | ||
| ) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| function hasInvalidPercentEncoding(input) { | ||
| return INVALID_PERCENT_REGEX.test(input); | ||
| } | ||
|
|
||
| function isIPvFuture(address) { | ||
| return IPV_FUTURE_REGEX.test(address); | ||
| } | ||
|
|
||
| function isValidAuthority(authority, options) { | ||
| const allowEmptyAuthority = Boolean(options && options.allowEmptyAuthority); | ||
| if (authority === '') { | ||
| return !!allowEmptyAuthority; | ||
| } | ||
|
|
||
| let hostPort = authority; | ||
| let userinfo = ''; | ||
| const atIndex = authority.lastIndexOf('@'); | ||
|
|
||
| if (atIndex !== -1) { | ||
| userinfo = authority.slice(0, atIndex); | ||
| hostPort = authority.slice(atIndex + 1); | ||
|
|
||
| if (!USERINFO_REGEX.test(userinfo)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| let host = hostPort; | ||
| let port = null; | ||
| let hasHost = false; | ||
|
|
||
| if (hostPort.startsWith('[')) { | ||
| const closingIndex = hostPort.indexOf(']'); | ||
|
|
||
| if (closingIndex === -1) { | ||
| return false; | ||
| } | ||
|
|
||
| const address = hostPort.slice(1, closingIndex); | ||
|
|
||
| if (!isIP(address, 6) && !isIPvFuture(address)) { | ||
| return false; | ||
| } | ||
|
|
||
| const remainder = hostPort.slice(closingIndex + 1); | ||
|
|
||
| if (remainder) { | ||
| if (!remainder.startsWith(':')) { | ||
| return false; | ||
| } | ||
|
|
||
| port = remainder.slice(1); | ||
| } | ||
|
|
||
| hasHost = true; | ||
| } else { | ||
| const firstColon = hostPort.indexOf(':'); | ||
| const lastColon = hostPort.lastIndexOf(':'); | ||
|
|
||
| if (firstColon !== lastColon) { | ||
| return false; | ||
| } | ||
|
|
||
| if (lastColon !== -1) { | ||
| host = hostPort.slice(0, lastColon); | ||
| port = hostPort.slice(lastColon + 1); | ||
| } | ||
|
|
||
| if (host) { | ||
| hasHost = true; | ||
|
|
||
| if (!isIP(host, 4) && !REG_NAME_REGEX.test(host)) { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (!hasHost) { | ||
| return false; | ||
| } | ||
|
|
||
| if (port !== null) { | ||
| if (port === '' || !/^[0-9]+$/.test(port)) { | ||
| return false; | ||
| } | ||
|
|
||
| const portNumber = parseInt(port, 10); | ||
|
|
||
| if (Number.isNaN(portNumber) || portNumber > 65535) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| function isValidPath(path, { hasAuthority, hasScheme }) { | ||
| if (hasAuthority) { | ||
| return PATH_ABEMPTY_REGEX.test(path); | ||
| } | ||
|
|
||
| if (hasScheme) { | ||
| if (path === '') { | ||
| return true; | ||
| } | ||
|
|
||
| if (path.startsWith('/')) { | ||
| return PATH_ABSOLUTE_REGEX.test(path); | ||
| } | ||
|
|
||
| return PATH_ROOTLESS_REGEX.test(path); | ||
| } | ||
|
|
||
| if (path === '') { | ||
| return true; | ||
| } | ||
|
|
||
| if (path.startsWith('/')) { | ||
| return PATH_ABSOLUTE_REGEX.test(path); | ||
| } | ||
|
|
||
| return PATH_NOSCHEME_REGEX.test(path); | ||
| } | ||
|
|
||
| function isValidQueryOrFragment(value) { | ||
| return value === '' || QUERY_FRAGMENT_REGEX.test(value); | ||
| } | ||
|
|
||
| function isValidUriReference(value) { | ||
| let rest = value; | ||
| let scheme = null; | ||
| let hadScheme = false; | ||
|
|
||
| const colonIndex = rest.indexOf(':'); | ||
|
|
||
| if (colonIndex > 0) { | ||
| const potentialScheme = rest.slice(0, colonIndex); | ||
|
|
||
| if (SCHEME_REGEX.test(potentialScheme)) { | ||
| scheme = potentialScheme; | ||
| hadScheme = true; | ||
| rest = rest.slice(colonIndex + 1); | ||
| } | ||
| } | ||
|
|
||
| let fragment = ''; | ||
| const hashIndex = rest.indexOf('#'); | ||
|
|
||
| if (hashIndex !== -1) { | ||
| fragment = rest.slice(hashIndex + 1); | ||
| rest = rest.slice(0, hashIndex); | ||
|
|
||
| if (!isValidQueryOrFragment(fragment)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| let query = ''; | ||
| const questionIndex = rest.indexOf('?'); | ||
|
|
||
| if (questionIndex !== -1) { | ||
| query = rest.slice(questionIndex + 1); | ||
| rest = rest.slice(0, questionIndex); | ||
|
|
||
| if (!isValidQueryOrFragment(query)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| let hasAuthority = false; | ||
| let authority = ''; | ||
| let path = rest; | ||
|
|
||
| if (rest.startsWith('//')) { | ||
| hasAuthority = true; | ||
| rest = rest.slice(2); | ||
| const nextSlash = rest.indexOf('/'); | ||
|
|
||
| if (nextSlash === -1) { | ||
| authority = rest; | ||
| path = ''; | ||
| } else { | ||
| authority = rest.slice(0, nextSlash); | ||
| path = rest.slice(nextSlash); | ||
| } | ||
|
|
||
| const allowEmptyAuthority = Boolean(hadScheme && scheme && scheme.toLowerCase() === 'file'); | ||
| const authorityOptions = allowEmptyAuthority | ||
| ? { allowEmptyAuthority: true } | ||
| : undefined; | ||
|
|
||
| if (!isValidAuthority(authority, authorityOptions)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return isValidPath(path, { hasAuthority, hasScheme: hadScheme }); | ||
| } | ||
|
|
||
| export default function isXsdAnyURI(input) { | ||
| assertString(input); | ||
|
|
||
| let value = collapseXmlWhitespace(input); | ||
|
|
||
| if (value === '') { | ||
| return true; | ||
| } | ||
|
|
||
| if ( | ||
| containsForbiddenControl(value) || | ||
| hasInvalidPercentEncoding(value) || | ||
| BACKSLASH_REGEX.test(value) || | ||
| DISALLOWED_ASCII_REGEX.test(value) | ||
| ) { | ||
| return false; | ||
| } | ||
|
|
||
| let encoded; | ||
|
|
||
| try { | ||
| const bracketSafeValue = value | ||
| .replace(/\[/g, OPEN_BRACKET_PLACEHOLDER) | ||
| .replace(/\]/g, CLOSE_BRACKET_PLACEHOLDER); | ||
|
|
||
| const encodedWithPlaceholders = encodeURI(bracketSafeValue); | ||
|
|
||
| encoded = encodedWithPlaceholders | ||
| .split(OPEN_BRACKET_PLACEHOLDER) | ||
| .join('[') | ||
| .split(CLOSE_BRACKET_PLACEHOLDER) | ||
| .join(']'); | ||
| } catch (err) { | ||
| return false; | ||
| } | ||
|
|
||
| return isValidUriReference(encoded); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1017,6 +1017,69 @@ describe('Validators', () => { | |
| }); | ||
| }); | ||
|
|
||
| it('should validate XML Schema AnyURI values', () => { | ||
| test({ | ||
| validator: 'isXsdAnyURI', | ||
| valid: [ | ||
| 'http://example.com', | ||
| 'https://example.com:8080/path?query=1#frag', | ||
| 'mailto:user@example.com', | ||
| 'urn:isbn:0451450523', | ||
| 'data:text/plain;charset=utf-8,Hello%20World', | ||
| '../relative/path', | ||
| '/absolute/path', | ||
| '//cdn.example.com/libs.js', | ||
| '#fragment-only', | ||
| '?queryOnly=true', | ||
| 'file:///C:/Program%20Files/MyApp/app.exe', | ||
| 'http://[2001:db8::1]:443/path', | ||
| 'http://[v7.fe80::abcd]/resource', | ||
| 'https://user:pa%20ss@example.com:8443/resource', | ||
| ' https://example.com/with-space ', | ||
| ' \t\nhttps://example.com/resource\r\n', | ||
| 'foo%20bar/baz', | ||
| 'tel:+123456789', | ||
| 'foo:', | ||
| 'foo:/bar', | ||
| 'file:///var/log', | ||
| 'http://[2001:db8::1]:1234', | ||
| '', | ||
| 'file:///', | ||
| '//example.com/path#frag', | ||
| ], | ||
| invalid: [ | ||
| 'http://example.com:99999', | ||
| 'http://example.com:port', | ||
| 'http://example.com:-1', | ||
| 'http://[::1', | ||
| 'http://example.com#frag#extra', | ||
| 'foo%zz', | ||
| 'foo%2', | ||
| 'http://user@:8080', | ||
| 'http://user[info@example.com', | ||
| '\\server\\share', | ||
| 'http://example.com/pa|th', | ||
| 'http://example.com/path\u0006', | ||
| '//:8080/path', | ||
| 'http:///path', | ||
| 'file://user@', | ||
| 'http://example.com/%', | ||
| 'foo#frag%2', | ||
| 'http://example.com/%ZZ', | ||
| 'http://example.com/?q=abc^123', | ||
| 'http://example.com?foo[bar', | ||
| 'foo://?query', | ||
| 'foo%2/bar', | ||
| 'http://[::g]/path', | ||
| 'http://[::1]foo', | ||
| 'http://host:80:123/path', | ||
| 'http://exa[mple.com', | ||
| 'http://example.com/\ud800', | ||
| 'foo<bar', | ||
| ], | ||
| }); | ||
|
Comment on lines
+1050
to
+1080
|
||
| }); | ||
|
|
||
| it('should validate MAC addresses', () => { | ||
| test({ | ||
| validator: 'isMACAddress', | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Port 0 should be rejected as invalid. The validation currently only checks if
portNumber > 65535but should also check ifportNumber < 1. Port 0 is reserved and not a valid port number for actual use.Suggested fix:
Note: The existing
isURLvalidator in this codebase also validatesport <= 0at line 241 ofsrc/lib/isURL.js.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rubiin I think for anyURI we aim to validate RFC 3986 syntax rather than protocol-specific semantics. RFC 3986 only requires that the port be digits; it doesn’t forbid 0. So Should we strictly follow the schema definition, or block it because it's technically invalid for network connections?