diff options
Diffstat (limited to 'src/validators.js')
| -rw-r--r-- | src/validators.js | 104 |
1 files changed, 103 insertions, 1 deletions
diff --git a/src/validators.js b/src/validators.js index 1c9ce9e3..7dcd7b8c 100644 --- a/src/validators.js +++ b/src/validators.js @@ -22,6 +22,14 @@ export function setValidatorCreatorMeta(validator, creator, meta) { return validator; } +// External configuration + +let disabledCuratedURLValidation = false; + +export function disableCuratedURLValidation() { + disabledCuratedURLValidation = true; +} + // Basic types (primitives) export function a(noun) { @@ -710,7 +718,101 @@ export function isName(name) { export function isURL(string) { isStringNonEmpty(string); - new URL(string); + // This might error. + const url = new URL(string); + + // This might, too. + decodeURIComponent(url.pathname); + + return true; +} + +export function isCuratedURL(string) { + if (disabledCuratedURLValidation) { + return isURL(string); + } + + // Do the same basic checks as in isURL. + // We'll need access to the URL object. + const url = new URL(string); + decodeURIComponent(url.pathname); + + const useHostname = hostname => + new Error( + `Use ${colors.green(hostname)}, ` + + `not ${colors.red(url.hostname)}`); + + switch (url.hostname) { + case 'tumblr.com': + throw useHostname('www.tumblr.com'); + + case 'www.twitter.com': + case 'x.com': + throw useHostname('twitter.com'); + + case 'youtu.be': + case 'youtube.com': + throw useHostname('www.youtube.com'); + } + + const dropSearchParam = (param, message = null) => { + if (url.searchParams.has(param)) { + const index = + Array.from(url.searchParams) + .findIndex(entry => entry[0] === param); + + const prefix = (index === 0 ? '?' : '&'); + + const value = url.searchParams.get(param); + + throw new Error( + `Remove ${colors.red(`${prefix}${param}=${value}`)}` + + (message ? ` (${message})` : '')); + } + }; + + const dropTrailingSlash = () => { + if (url.pathname.length >= 3 && url.pathname.endsWith('/')) { + throw new Error( + `Remove slash at end: ` + + url.pathname.slice(0, -1) + + colors.bright(colors.red('/'))); + } + }; + + switch (url.hostname) { + case 'soundcloud.com': + dropTrailingSlash(); + break; + + case 'open.spotify.com': + dropSearchParam('si', `tracking parameter`); + dropSearchParam('nd', `unnecessary parameter`); + break; + + case 'www.youtube.com': + dropSearchParam('si', `tracking parameter`); + break; + } + + if (url.hostname === 'music.apple.com') { + const countryMatch = + url.pathname.match(/^\/[a-z][a-z]\//); + + if (countryMatch) { + throw new Error(`Remove country code ${colors.red(countryMatch[0])}`); + } + } + + if (url.pathname === '/' && string === url.origin + url.hash + url.search) { + if (url.hash) { + throw new Error(`Add slash before "#" hash`); + } else if (url.search) { + throw new Error(`Add slash before "?" query`); + } else { + throw new Error(`Add slash at end`); + } + } return true; } |