« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/validators.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/validators.js')
-rw-r--r--src/validators.js104
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;
 }