Repository: validatorjs/validator.js
Pull request #1777 (2021-10-12 12:51:15)

fix(isUUID): handle of null version value


<!--- briefly describe what you have done in this PR --->
The issue here lies in that, where the defaulting of argument is only managed for when the value of the version is 'undefined', and for other falsy values (in my case 'null') it accepts them. There are cases where functions do trigger callbacks with null values instead of undefined, and it usually leads to unexpected outcomes. I do believe that this is the general understanding of how the function should be used, when the version argument is falsy, it should check for 'all' pattern.

The particular issue is here:

<img width="717" alt="Screenshot 2021-10-12 at 13 48 29" src="https://user-images.githubusercontent.com/31168388/136959544-b3d5cb01-6074-4676-998d-3d5696331f0f.png">

## Checklist

- [x] PR contains only changes related; no stray files, etc.
- [ ] README updated (where applicable)
- [x] Tests written (where applicable)



Patch:
diff --git a/README.md b/README.md
index 1ff1e00..4842d66 100644
--- a/README.md
+++ b/README.md
@@ -107,7 +107,7 @@ Validator                               | Description
 **isEmpty(str [, options])**            | check if the string has a length of zero.<br/><br/>`options` is an object which defaults to `{ ignore_whitespace:false }`.
 **isEthereumAddress(str)**              | check if the string is an [Ethereum](https://ethereum.org/) address using basic regex. Does not validate address checksums.
 **isFloat(str [, options])**            | check if the string is a float.<br/><br/>`options` is an object which can contain the keys `min`, `max`, `gt`, and/or `lt` to validate the float is within boundaries (e.g. `{ min: 7.22, max: 9.55 }`) it also has `locale` as an option.<br/><br/>`min` and `max` are equivalent to 'greater or equal' and 'less or equal', respectively while `gt` and `lt` are their strict counterparts.<br/><br/>`locale` determine the decimal separator and is one of `['ar', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-LB', 'ar-LY', 'ar-MA', 'ar-QA', 'ar-QM', 'ar-SA', 'ar-SD', 'ar-SY', 'ar-TN', 'ar-YE', 'bg-BG', 'cs-CZ', 'da-DK', 'de-DE', 'en-AU', 'en-GB', 'en-HK', 'en-IN', 'en-NZ', 'en-US', 'en-ZA', 'en-ZM', 'es-ES', 'fr-CA', 'fr-FR', 'hu-HU', 'it-IT', 'nb-NO', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-BR', 'pt-PT', 'ru-RU', 'sl-SI', 'sr-RS', 'sr-RS@latin', 'sv-SE', 'tr-TR', 'uk-UA']`. Locale list is `validator.isFloatLocales`.
-**isFQDN(str [, options])**             | check if the string is a fully qualified domain name (e.g. domain.com).<br/><br/>`options` is an object which defaults to `{ require_tld: true, allow_underscores: false, allow_trailing_dot: false , allow_numeric_tld: false }`.
+**isFQDN(str [, options])**             | check if the string is a fully qualified domain name (e.g. domain.com).<br/><br/>`options` is an object which defaults to `{ require_tld: true, allow_underscores: false, allow_trailing_dot: false, allow_numeric_tld: false, allow_wildcard: false }`. If `allow_wildcard` is set to true, the validator will allow domain starting with `*.` (e.g. `*.example.com` or `*.shop.example.com`).
 **isFullWidth(str)**                    | check if the string contains any full-width chars.
 **isHalfWidth(str)**                    | check if the string contains any half-width chars.
 **isHash(str, algorithm)**              | check if the string is a hash of type algorithm.<br/><br/>Algorithm is one of `['md4', 'md5', 'sha1', 'sha256', 'sha384', 'sha512', 'ripemd128', 'ripemd160', 'tiger128', 'tiger160', 'tiger192', 'crc32', 'crc32b']`
@@ -140,7 +140,7 @@ Validator                               | Description
 **isMagnetURI(str)**                      | check if the string is a [magnet uri format](https://en.wikipedia.org/wiki/Magnet_URI_scheme).
 **isMD5(str)**                          | check if the string is a MD5 hash.<br/><br/>Please note that you can also use the `isHash(str, 'md5')` function. Keep in mind that MD5 has some collision weaknesses compared to other algorithms (e.g., SHA).
 **isMimeType(str)**                     | check if the string matches to a valid [MIME type](https://en.wikipedia.org/wiki/Media_type) format
-**isMobilePhone(str [, locale [, options]])**          | check if the string is a mobile phone number,<br/><br/>(locale is either an array of locales (e.g `['sk-SK', 'sr-RS']`) OR one of `['am-Am', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', ar-JO', 'ar-KW', 'ar-SA', 'ar-SY', 'ar-TN', 'az-AZ', 'az-LY', 'az-LB', 'bs-BA', 'be-BY', 'bg-BG', 'bn-BD', 'ca-AD', 'cs-CZ', 'da-DK', 'de-DE', 'de-AT', 'de-CH', 'de-LU', 'el-GR', 'en-AU', 'en-CA', 'en-GB', 'en-GG', 'en-GH', 'en-HK', 'en-MO', 'en-IE', 'en-IN', 'en-KE', 'en-MT', 'en-MU', 'en-NG', 'en-NZ', 'en-PK', 'en-PH', 'en-RW', 'en-SG', 'en-SL', 'en-UG', 'en-US', 'en-TZ', 'en-ZA', 'en-ZM', 'en-ZW', 'es-AR', 'es-BO', 'es-CL', 'es-CO', 'es-CR', 'es-DO', 'es-HN', 'es-PE', 'es-EC', 'es-ES', 'es-MX', 'es-PA', 'es-PY', 'es-UY', 'es-VE', 'et-EE', 'fa-IR', 'fi-FI', 'fj-FJ', 'fo-FO', 'fr-BE', 'fr-FR', 'fr-GF', 'fr-GP', 'fr-MQ', 'fr-RE', 'ga-IE', 'he-IL', 'hu-HU', 'id-ID', 'it-IT', 'it-SM', 'ja-JP', 'ka-GE', 'kk-KZ', 'kl-GL', 'ko-KR', 'lt-LT', 'ms-MY', ''mz-MZ', nb-NO', 'ne-NP', 'nl-BE', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-BR', 'pt-PT', 'pt-AO', 'ro-RO', 'ru-RU', 'si-LK' 'sl-SI', 'sk-SK', 'sq-AL', 'sr-RS', 'sv-SE', 'th-TH', 'tr-TR', 'uk-UA', 'uz-UZ', 'vi-VN', 'zh-CN', 'zh-HK', 'zh-MO', 'zh-TW']` OR defaults to 'any'. If 'any' or a falsey value is used, function will check if any of the locales match).<br/><br/>`options` is an optional object that can be supplied with the following keys: `strictMode`, if this is set to `true`, the mobile phone number must be supplied with the country code and therefore must start with `+`. Locale list is `validator.isMobilePhoneLocales`.
+**isMobilePhone(str [, locale [, options]])**          | check if the string is a mobile phone number,<br/><br/>(locale is either an array of locales (e.g `['sk-SK', 'sr-RS']`) OR one of `['am-Am', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', ar-JO', 'ar-KW', 'ar-SA', 'ar-SY', 'ar-TN', 'az-AZ', 'az-LY', 'az-LB', 'bs-BA', 'be-BY', 'bg-BG', 'bn-BD', 'ca-AD', 'cs-CZ', 'da-DK', 'de-DE', 'de-AT', 'de-CH', 'de-LU', 'el-GR', 'en-AU', 'en-CA', 'en-GB', 'en-GG', 'en-GH', 'en-HK', 'en-MO', 'en-IE', 'en-IN', 'en-KE', 'en-MT', 'en-MU', 'en-NG', 'en-NZ', 'en-PK', 'en-PH', 'en-RW', 'en-SG', 'en-SL', 'en-UG', 'en-US', 'en-TZ', 'en-ZA', 'en-ZM', 'en-ZW', 'es-AR', 'es-BO', 'es-CL', 'es-CO', 'es-CR', 'es-CU', 'es-DO', 'es-HN', 'es-PE', 'es-EC', 'es-ES', 'es-MX', 'es-PA', 'es-PY', 'es-UY', 'es-VE', 'et-EE', 'fa-IR', 'fi-FI', 'fj-FJ', 'fo-FO', 'fr-BE', 'fr-FR', 'fr-GF', 'fr-GP', 'fr-MQ', 'fr-RE', 'ga-IE', 'he-IL', 'hu-HU', 'id-ID', 'it-IT', 'it-SM', 'ja-JP', 'ka-GE', 'kk-KZ', 'kl-GL', 'ko-KR', 'lt-LT', 'ms-MY', ''mz-MZ', nb-NO', 'ne-NP', 'nl-BE', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-BR', 'pt-PT', 'pt-AO', 'ro-RO', 'ru-RU', 'si-LK' 'sl-SI', 'sk-SK', 'sq-AL', 'sr-RS', 'sv-SE', 'th-TH', 'tr-TR', 'uk-UA', 'uz-UZ', 'vi-VN', 'zh-CN', 'zh-HK', 'zh-MO', 'zh-TW']` OR defaults to 'any'. If 'any' or a falsey value is used, function will check if any of the locales match).<br/><br/>`options` is an optional object that can be supplied with the following keys: `strictMode`, if this is set to `true`, the mobile phone number must be supplied with the country code and therefore must start with `+`. Locale list is `validator.isMobilePhoneLocales`.
 **isMongoId(str)**                      | check if the string is a valid hex-encoded representation of a [MongoDB ObjectId][mongoid].
 **isMultibyte(str)**                    | check if the string contains one or more multibyte chars.
 **isNumeric(str [, options])**                      | check if the string contains only numbers.<br/><br/>`options` is an object which defaults to `{no_symbols: false}` it also has locale as an option. If `no_symbols` is true, the validator will reject numeric strings that feature a symbol (e.g. `+`, `-`, or `.`).<br/><br/>`locale` determine the decimal separator and is one of `['ar', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-LB', 'ar-LY', 'ar-MA', 'ar-QA', 'ar-QM', 'ar-SA', 'ar-SD', 'ar-SY', 'ar-TN', 'ar-YE', 'bg-BG', 'cs-CZ', 'da-DK', 'de-DE', 'en-AU', 'en-GB', 'en-HK', 'en-IN', 'en-NZ', 'en-US', 'en-ZA', 'en-ZM', 'es-ES', 'fr-FR', 'fr-CA', 'hu-HU', 'it-IT', 'nb-NO', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-BR', 'pt-PT', 'ru-RU', 'sl-SI', 'sr-RS', 'sr-RS@latin', 'sv-SE', 'tr-TR', 'uk-UA']`.
diff --git a/src/lib/isFQDN.js b/src/lib/isFQDN.js
index 3dff7d2..b82b089 100644
--- a/src/lib/isFQDN.js
+++ b/src/lib/isFQDN.js
@@ -6,6 +6,7 @@ const default_fqdn_options = {
   allow_underscores: false,
   allow_trailing_dot: false,
   allow_numeric_tld: false,
+  allow_wildcard: false,
 };
 
 export default function isFQDN(str, options) {
@@ -16,6 +17,12 @@ export default function isFQDN(str, options) {
   if (options.allow_trailing_dot && str[str.length - 1] === '.') {
     str = str.substring(0, str.length - 1);
   }
+
+  /* Remove the optional wildcard before checking validity */
+  if (options.allow_wildcard === true && str.indexOf('*.') === 0) {
+    str = str.substring(2);
+  }
+
   const parts = str.split('.');
   const tld = parts[parts.length - 1];
 
diff --git a/src/lib/isMobilePhone.js b/src/lib/isMobilePhone.js
index 8b56b76..1700200 100644
--- a/src/lib/isMobilePhone.js
+++ b/src/lib/isMobilePhone.js
@@ -59,6 +59,7 @@ const phones = {
   'es-CO': /^(\+?57)?3(0(0|1|2|4|5)|1\d|2[0-4]|5(0|1))\d{7}$/,
   'es-CL': /^(\+?56|0)[2-9]\d{1}\d{7}$/,
   'es-CR': /^(\+506)?[2-8]\d{7}$/,
+  'es-CU': /^(\+53|0053)?5\d{7}/,
   'es-DO': /^(\+?1)?8[024]9\d{7}$/,
   'es-HN': /^(\+?504)?[9|8]\d{7}$/,
   'es-EC': /^(\+?593|0)([2-7]|9[2-9])\d{7}$/,
@@ -115,7 +116,7 @@ const phones = {
   'uk-UA': /^(\+?38|8)?0\d{9}$/,
   'uz-UZ': /^(\+?998)?(6[125-79]|7[1-69]|88|9\d)\d{7}$/,
   'vi-VN': /^((\+?84)|0)((3([2-9]))|(5([25689]))|(7([0|6-9]))|(8([1-9]))|(9([0-9])))([0-9]{7})$/,
-  'zh-CN': /^((\+|00)86)?1([3456789][0-9]|4[579]|6[2567]|7[01235678]|9[012356789])[0-9]{8}$/,
+  'zh-CN': /^((\+|00)86)?(1[3-9]|9[28])\d{9}$/,
   'zh-TW': /^(\+?886\-?|0)?9\d{8}$/,
 };
 /* eslint-enable max-len */
diff --git a/src/lib/isURL.js b/src/lib/isURL.js
index 7f6e0ba..253b309 100644
--- a/src/lib/isURL.js
+++ b/src/lib/isURL.js
@@ -136,7 +136,7 @@ export default function isURL(url, options) {
     }
   }
 
-  if (port_str !== null) {
+  if (port_str !== null && port_str.length > 0) {
     port = parseInt(port_str, 10);
     if (!/^[0-9]+$/.test(port_str) || port <= 0 || port > 65535) {
       return false;
diff --git a/src/lib/isUUID.js b/src/lib/isUUID.js
index 61d938a..8013cfb 100644
--- a/src/lib/isUUID.js
+++ b/src/lib/isUUID.js
@@ -7,8 +7,8 @@ const uuid = {
   all: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i,
 };
 
-export default function isUUID(str, version = 'all') {
+export default function isUUID(str, version) {
   assertString(str);
-  const pattern = uuid[version];
+  const pattern = uuid[![undefined, null].includes(version) ? version : 'all'];
   return pattern && pattern.test(str);
 }


Test patch:
diff --git a/test/validators.js b/test/validators.js
index 91794d5..64aa67d 100644
--- a/test/validators.js
+++ b/test/validators.js
@@ -535,6 +535,31 @@ describe('Validators', () => {
     });
   });
 
+  it('should validate URLs with column and no port', () => {
+    test({
+      validator: 'isURL',
+      valid: [
+        'http://example.com:',
+        'ftp://example.com:',
+      ],
+      invalid: [
+        'https://example.com:abc',
+      ],
+    });
+  });
+
+  it('should validate sftp protocol URL containing column and no port', () => {
+    test({
+      validator: 'isURL',
+      args: [{
+        protocols: ['sftp'],
+      }],
+      valid: [
+        'sftp://user:pass@terminal.aws.test.nl:/incoming/things.csv',
+      ],
+    });
+  });
+
   it('should validate protocol relative URLs', () => {
     test({
       validator: 'isURL',
@@ -1118,6 +1143,18 @@ describe('Validators', () => {
       ],
     });
   });
+  it('should validate FQDN with wildcard option', () => {
+    test({
+      validator: 'isFQDN',
+      args: [
+        { allow_wildcard: true },
+      ],
+      valid: [
+        '*.example.com',
+        '*.shop.example.com',
+      ],
+    });
+  });
 
   it('should validate alpha strings', () => {
     test({
@@ -4413,6 +4450,34 @@ describe('Validators', () => {
         'AAAAAAAA-1111-1111-AAAG-111111111111',
       ],
     });
+    test({
+      validator: 'isUUID',
+      args: [undefined],
+      valid: [
+        'A117FBC9-4BED-3078-CF07-9141BA07C9F3',
+        'A117FBC9-4BED-5078-AF07-9141BA07C9F3',
+      ],
+      invalid: [
+        '',
+        'xxxA987FBC9-4BED-3078-CF07-9141BA07C9F3',
+        'A987FBC94BED3078CF079141BA07C9F3',
+        'A11AAAAA-1111-1111-AAAG-111111111111',
+      ],
+    });
+    test({
+      validator: 'isUUID',
+      args: [null],
+      valid: [
+        'A127FBC9-4BED-3078-CF07-9141BA07C9F3',
+      ],
+      invalid: [
+        '',
+        'xxxA987FBC9-4BED-3078-CF07-9141BA07C9F3',
+        'A127FBC9-4BED-3078-CF07-9141BA07C9F3xxx',
+        '912859',
+        'A12AAAAA-1111-1111-AAAG-111111111111',
+      ],
+    });
     test({
       validator: 'isUUID',
       args: [3],
@@ -6102,19 +6167,28 @@ describe('Validators', () => {
       {
         locale: 'zh-CN',
         valid: [
-          '15323456787',
           '13523333233',
-          '13898728332',
+          '13838389438',
+          '14899230918',
+          '14999230918',
+          '15323456787',
+          '15052052020',
+          '16237108167',
+          '008616238234822',
+          '+8616238234822',
+          '16565600001',
+          '17269427292',
+          '17469427292',
+          '18199617480',
+          '19151751717',
+          '19651751717',
           '+8613238234822',
           '+8613487234567',
           '+8617823492338',
           '+8617823492338',
-          '16637108167',
           '+8616637108167',
           '+8616637108167',
           '+8616712341234',
-          '008618812341234',
-          '008618812341234',
           '+8619912341234',
           '+8619812341234',
           '+8619712341234',
@@ -6123,17 +6197,25 @@ describe('Validators', () => {
           '+8619312341234',
           '+8619212341234',
           '+8619112341234',
-          '17269427292',
-          '16565600001',
           '+8617269427292',
+          '008618812341234',
+          '008618812341234',
           '008617269427292',
-          '16238234822',
-          '008616238234822',
-          '+8616238234822',
+          // Reserve number segments in the future.
+          '92138389438',
+          '+8692138389438',
+          '008692138389438',
+          '98199649964',
+          '+8698099649964',
+          '008698099649964',
         ],
         invalid: [
           '12345',
           '',
+          '12038389438',
+          '12838389438',
+          '013838389438',
+          '+86-13838389438',
           '+08613811211114',
           '+008613811211114',
           '08613811211114',
@@ -7016,6 +7098,34 @@ describe('Validators', () => {
           '01234567',
         ],
       },
+      {
+        locale: 'es-CU',
+        valid: [
+          '+5351234567',
+          '005353216547',
+          '51234567',
+          '53214567',
+        ],
+        invalid: [
+          '1234',
+          '+5341234567',
+          '0041234567',
+          '41234567',
+          '11234567',
+          '21234567',
+          '31234567',
+          '60303456',
+          '71234567',
+          '81234567',
+          '91234567',
+          '+5343216547',
+          '+5332165498',
+          '+53121234567',
+          '',
+          'abc',
+          '+535123457',
+        ],
+      },
       {
         locale: 'es-DO',
         valid: [