diff --git a/README.md b/README.md index 7c1cadb9f..791fb53c5 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Validator | Description **isDecimal(str [, options])** | check if the string represents a decimal number, such as 0.1, .3, 1.1, 1.00003, 4.0, etc.

`options` is an object which defaults to `{force_decimal: false, decimal_digits: '1,', locale: 'en-US'}`.

`locale` determines 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', 'el-GR', 'en-AU', 'en-GB', 'en-HK', 'en-IN', 'en-NZ', 'en-US', 'en-ZA', 'en-ZM', 'eo', 'es-ES', 'fa', 'fa-AF', 'fa-IR', 'fr-FR', 'fr-CA', 'hu-HU', 'id-ID', 'it-IT', 'ku-IQ', 'nb-NO', 'nl-NL', 'nn-NO', 'pl-PL', 'pl-Pl', 'pt-BR', 'pt-PT', 'ru-RU', 'sl-SI', 'sr-RS', 'sr-RS@latin', 'sv-SE', 'tr-TR', 'uk-UA', 'vi-VN']`.
**Note:** `decimal_digits` is given as a range like '1,3', a specific value like '3' or min like '1,'. **isDivisibleBy(str, number)** | check if the string is a number that is divisible by another. **isEAN(str)** | check if the string is an [EAN (European Article Number)][European Article Number]. -**isEmail(str [, options])** | check if the string is an email.

`options` is an object which defaults to `{ allow_display_name: false, require_display_name: false, allow_utf8_local_part: true, require_tld: true, allow_ip_domain: false, allow_underscores: false, domain_specific_validation: false, blacklisted_chars: '', host_blacklist: [] }`. If `allow_display_name` is set to true, the validator will also match `Display Name `. If `require_display_name` is set to true, the validator will reject strings without the format `Display Name `. If `allow_utf8_local_part` is set to false, the validator will not allow any non-English UTF8 character in email address' local part. If `require_tld` is set to false, email addresses without a TLD in their domain will also be matched. If `ignore_max_length` is set to true, the validator will not check for the standard max length of an email. If `allow_ip_domain` is set to true, the validator will allow IP addresses in the host part. If `domain_specific_validation` is true, some additional validation will be enabled, e.g. disallowing certain syntactically valid email addresses that are rejected by Gmail. If `blacklisted_chars` receives a string, then the validator will reject emails that include any of the characters in the string, in the name part. If `host_blacklist` is set to an array of strings and the part of the email after the `@` symbol matches one of the strings defined in it, the validation fails. If `host_whitelist` is set to an array of strings and the part of the email after the `@` symbol matches none of the strings defined in it, the validation fails. +**isEmail(str [, options])** | check if the string is an email.

`options` is an object which defaults to `{ allow_display_name: false, require_display_name: false, allow_utf8_local_part: true, require_tld: true, allow_ip_domain: false, allow_underscores: false, domain_specific_validation: false, blacklisted_chars: '', host_blacklist: [] }`. If `allow_display_name` is set to true, the validator will also match `Display Name `. If `require_display_name` is set to true, the validator will reject strings without the format `Display Name `. If `allow_utf8_local_part` is set to false, the validator will not allow any non-English UTF8 character in email address' local part. If `require_tld` is set to false, email addresses without a TLD in their domain will also be matched. If `ignore_max_length` is set to true, the validator will not check for the standard max length of an email. If `allow_ip_domain` is set to true, the validator will allow IP addresses in the host part. If `domain_specific_validation` is true, some additional validation will be enabled, e.g. disallowing certain syntactically valid email addresses that are rejected by Gmail. If `blacklisted_chars` receives a string, then the validator will reject emails that include any of the characters in the string, in the name part. If `host_blacklist` is set to an array of strings or regexp, and the part of the email after the `@` symbol matches one of the strings defined in it, the validation fails. If `host_whitelist` is set to an array of strings or regexp, and the part of the email after the `@` symbol matches none of the strings defined in it, the validation fails. **isEmpty(str [, options])** | check if the string has a length of zero.

`options` is an object which defaults to `{ ignore_whitespace: false }`. **isEthereumAddress(str)** | check if the string is an [Ethereum][Ethereum] address. Does not validate address checksums. **isFloat(str [, options])** | check if the string is a float.

`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.

`min` and `max` are equivalent to 'greater or equal' and 'less or equal', respectively while `gt` and `lt` are their strict counterparts.

`locale` determines 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', 'eo', '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`. @@ -139,7 +139,7 @@ Validator | Description **isJSON(str [, options])** | check if the string is valid JSON (note: uses JSON.parse).

`options` is an object which defaults to `{ allow_primitives: false }`. If `allow_primitives` is true, the primitives 'true', 'false' and 'null' are accepted as valid JSON values. **isJWT(str)** | check if the string is valid JWT token. **isLatLong(str [, options])** | check if the string is a valid latitude-longitude coordinate in the format `lat,long` or `lat, long`.

`options` is an object that defaults to `{ checkDMS: false }`. Pass `checkDMS` as `true` to validate DMS(degrees, minutes, and seconds) latitude-longitude format. -**isLength(str [, options])** | check if the string's length falls in a range.

`options` is an object which defaults to `{ min: 0, max: undefined }`. Note: this function takes into account surrogate pairs. +**isLength(str [, options])** | check if the string's length falls in a range and equal to any of the integers of the `discreteLengths` array if provided.

`options` is an object which defaults to `{ min: 0, max: undefined, discreteLengths: undefined }`. Note: this function takes into account surrogate pairs. **isLicensePlate(str, locale)** | check if the string matches the format of a country's license plate.

`locale` is one of `['cs-CZ', 'de-DE', 'de-LI', 'en-IN', 'en-SG', 'en-PK', 'es-AR', 'hu-HU', 'pt-BR', 'pt-PT', 'sq-AL', 'sv-SE']` or `'any'`. **isLocale(str)** | check if the string is a locale. **isLowercase(str)** | check if the string is lowercase. diff --git a/src/lib/isDate.js b/src/lib/isDate.js index ede3e33e6..3a1e4afd2 100644 --- a/src/lib/isDate.js +++ b/src/lib/isDate.js @@ -28,6 +28,7 @@ export default function isDate(input, options) { options = merge(options, default_date_options); } if (typeof input === 'string' && isValidFormat(options.format)) { + if (options.strictMode && input.length !== options.format.length) return false; const formatDelimiter = options.delimiters .find(delimiter => options.format.indexOf(delimiter) !== -1); const dateDelimiter = options.strictMode diff --git a/src/lib/isEmail.js b/src/lib/isEmail.js index 1aceca3cf..abe465052 100644 --- a/src/lib/isEmail.js +++ b/src/lib/isEmail.js @@ -1,4 +1,5 @@ import assertString from './util/assertString'; +import checkHost from './util/checkHost'; import isByteLength from './isByteLength'; import isFQDN from './isFQDN'; @@ -60,7 +61,6 @@ function validateDisplayName(display_name) { return true; } - export default function isEmail(str, options) { assertString(str); options = merge(options, default_email_options); @@ -97,11 +97,11 @@ export default function isEmail(str, options) { const domain = parts.pop(); const lower_domain = domain.toLowerCase(); - if (options.host_blacklist.includes(lower_domain)) { + if (options.host_blacklist.length > 0 && checkHost(lower_domain, options.host_blacklist)) { return false; } - if (options.host_whitelist.length > 0 && !options.host_whitelist.includes(lower_domain)) { + if (options.host_whitelist.length > 0 && !checkHost(lower_domain, options.host_whitelist)) { return false; } diff --git a/src/lib/isISO6346.js b/src/lib/isISO6346.js index 0cb657e7c..2c28c1123 100644 --- a/src/lib/isISO6346.js +++ b/src/lib/isISO6346.js @@ -27,7 +27,8 @@ export function isISO6346(str) { } else sum += str[i] * (2 ** i); } - const checkSumDigit = sum % 11; + let checkSumDigit = sum % 11; + if (checkSumDigit === 10) checkSumDigit = 0; return Number(str[str.length - 1]) === checkSumDigit; } diff --git a/src/lib/isLength.js b/src/lib/isLength.js index 4ef8b83eb..4d5d52546 100644 --- a/src/lib/isLength.js +++ b/src/lib/isLength.js @@ -5,6 +5,7 @@ export default function isLength(str, options) { assertString(str); let min; let max; + if (typeof (options) === 'object') { min = options.min || 0; max = options.max; @@ -12,8 +13,15 @@ export default function isLength(str, options) { min = arguments[1] || 0; max = arguments[2]; } + const presentationSequences = str.match(/(\uFE0F|\uFE0E)/g) || []; const surrogatePairs = str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g) || []; const len = str.length - presentationSequences.length - surrogatePairs.length; - return len >= min && (typeof max === 'undefined' || len <= max); + const isInsideRange = len >= min && (typeof max === 'undefined' || len <= max); + + if (isInsideRange && Array.isArray(options?.discreteLengths)) { + return options.discreteLengths.some(discreteLen => discreteLen === len); + } + + return isInsideRange; } diff --git a/src/lib/isMobilePhone.js b/src/lib/isMobilePhone.js index 71afda7fb..8e7909418 100644 --- a/src/lib/isMobilePhone.js +++ b/src/lib/isMobilePhone.js @@ -43,7 +43,7 @@ const phones = { 'en-BS': /^(\+?1[-\s]?|0)?\(?242\)?[-\s]?\d{3}[-\s]?\d{4}$/, 'en-GB': /^(\+?44|0)7[1-9]\d{8}$/, 'en-GG': /^(\+?44|0)1481\d{6}$/, - 'en-GH': /^(\+233|0)(20|50|24|54|27|57|26|56|23|28|55|59)\d{7}$/, + 'en-GH': /^(\+233|0)(20|50|24|54|27|57|26|56|23|53|28|55|59)\d{7}$/, 'en-GY': /^(\+592|0)6\d{6}$/, 'en-HK': /^(\+?852[-\s]?)?[456789]\d{3}[-\s]?\d{4}$/, 'en-MO': /^(\+?853[-\s]?)?[6]\d{3}[-\s]?\d{4}$/, @@ -74,7 +74,7 @@ const phones = { 'en-US': /^((\+1|1)?( |-)?)?(\([2-9][0-9]{2}\)|[2-9][0-9]{2})( |-)?([2-9][0-9]{2}( |-)?[0-9]{4})$/, 'en-VU': /^(\+678)?[5]\d{5}$/, 'en-ZA': /^(\+?27|0)\d{9}$/, - 'en-ZM': /^(\+?26)?09[567]\d{7}$/, + 'en-ZM': /^(\+?26)?0[79][567]\d{7}$/, 'en-ZW': /^(\+263)[0-9]{9}$/, 'en-BW': /^(\+?267)?(7[1-8]{1})\d{6}$/, 'es-AR': /^\+?549(11|[2368]\d)\d{8}$/, @@ -125,7 +125,7 @@ const phones = { 'kk-KZ': /^(\+?7|8)?7\d{9}$/, 'kl-GL': /^(\+?299)?\s?\d{2}\s?\d{2}\s?\d{2}$/, 'ko-KR': /^((\+?82)[ \-]?)?0?1([0|1|6|7|8|9]{1})[ \-]?\d{3,4}[ \-]?\d{4}$/, - 'ky-KG': /^(\+?7\s?\+?7|0)\s?\d{2}\s?\d{3}\s?\d{4}$/, + 'ky-KG': /^(\+996\s?)?(22[0-9]|50[0-9]|55[0-9]|70[0-9]|75[0-9]|77[0-9]|880|990|995|996|997|998)\s?\d{3}\s?\d{3}$/, 'lt-LT': /^(\+370|8)\d{8}$/, 'lv-LV': /^(\+?371)2\d{7}$/, 'mg-MG': /^((\+?261|0)(2|3)\d)?\d{7}$/, diff --git a/src/lib/isPostalCode.js b/src/lib/isPostalCode.js index 103656205..cadf39346 100644 --- a/src/lib/isPostalCode.js +++ b/src/lib/isPostalCode.js @@ -14,7 +14,7 @@ const patterns = { BA: /^([7-8]\d{4}$)/, BE: fourDigit, BG: fourDigit, - BR: /^\d{5}-\d{3}$/, + BR: /^\d{5}-?\d{3}$/, BY: /^2[1-4]\d{4}$/, CA: /^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJ-NPRSTV-Z][\s\-]?\d[ABCEGHJ-NPRSTV-Z]\d$/i, CH: fourDigit, diff --git a/src/lib/isURL.js b/src/lib/isURL.js index 7529f4bde..9bfafbf0c 100644 --- a/src/lib/isURL.js +++ b/src/lib/isURL.js @@ -1,4 +1,5 @@ import assertString from './util/assertString'; +import checkHost from './util/checkHost'; import isFQDN from './isFQDN'; import isIP from './isIP'; @@ -38,20 +39,6 @@ const default_url_options = { const wrapped_ipv6 = /^\[([^\]]+)\](?::([0-9]+))?$/; -function isRegExp(obj) { - return Object.prototype.toString.call(obj) === '[object RegExp]'; -} - -function checkHost(host, matches) { - for (let i = 0; i < matches.length; i++) { - let match = matches[i]; - if (host === match || (isRegExp(match) && match.test(host))) { - return true; - } - } - return false; -} - export default function isURL(url, options) { assertString(url); if (!url || /[\s<>]/.test(url)) { diff --git a/src/lib/util/checkHost.js b/src/lib/util/checkHost.js new file mode 100644 index 000000000..ed1dddefe --- /dev/null +++ b/src/lib/util/checkHost.js @@ -0,0 +1,13 @@ +function isRegExp(obj) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +} + +export default function checkHost(host, matches) { + for (let i = 0; i < matches.length; i++) { + let match = matches[i]; + if (host === match || (isRegExp(match) && match.test(host))) { + return true; + } + } + return false; +} diff --git a/test/validators.test.js b/test/validators.test.js index 3b6ede69a..aa13906b0 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -325,6 +325,25 @@ describe('Validators', () => { }); }); + it('should allow regular expressions in the host blacklist of isEmail', () => { + test({ + validator: 'isEmail', + args: [{ + host_blacklist: ['bar.com', 'foo.com', /\.foo\.com$/], + }], + valid: [ + 'email@foobar.com', + 'email@foo.bar.com', + 'email@qux.com', + ], + invalid: [ + 'email@bar.com', + 'email@foo.com', + 'email@a.b.c.foo.com', + ], + }); + }); + it('should validate only email addresses with whitelisted domains', () => { test({ validator: 'isEmail', @@ -341,6 +360,25 @@ describe('Validators', () => { }); }); + it('should allow regular expressions in the host whitelist of isEmail', () => { + test({ + validator: 'isEmail', + args: [{ + host_whitelist: ['bar.com', 'foo.com', /\.foo\.com$/], + }], + valid: [ + 'email@bar.com', + 'email@foo.com', + 'email@a.b.c.foo.com', + ], + invalid: [ + 'email@foobar.com', + 'email@foo.bar.com', + 'email@qux.com', + ], + }); + }); + it('should validate URLs', () => { test({ validator: 'isURL', @@ -5394,12 +5432,42 @@ describe('Validators', () => { valid: ['abc', 'de', 'a', ''], invalid: ['abcd'], }); + test({ + validator: 'isLength', + args: [{ max: 6, discreteLengths: 5 }], + valid: ['abcd', 'vfd', 'ff', '', 'k'], + invalid: ['abcdefgh', 'hfjdksks'], + }); + test({ + validator: 'isLength', + args: [{ min: 2, max: 6, discreteLengths: 5 }], + valid: ['bsa', 'vfvd', 'ff'], + invalid: ['', ' ', 'hfskdunvc'], + }); + test({ + validator: 'isLength', + args: [{ min: 1, discreteLengths: 2 }], + valid: [' ', 'hello', 'bsa'], + invalid: [''], + }); test({ validator: 'isLength', args: [{ max: 0 }], valid: [''], invalid: ['a', 'ab'], }); + test({ + validator: 'isLength', + args: [{ min: 5, max: 10, discreteLengths: [2, 6, 8, 9] }], + valid: ['helloguy', 'shopping', 'validator', 'length'], + invalid: ['abcde', 'abcdefg'], + }); + test({ + validator: 'isLength', + args: [{ discreteLengths: '9' }], + valid: ['a', 'abcd', 'abcdefghijkl'], + invalid: [], + }); test({ validator: 'isLength', valid: ['a', '', 'asds'], @@ -8023,6 +8091,7 @@ describe('Validators', () => { '0502345671', '0242345671', '0542345671', + '0532345671', '0272345671', '0572345671', '0262345671', @@ -8033,6 +8102,7 @@ describe('Validators', () => { '+233502345671', '+233242345671', '+233542345671', + '+233532345671', '+233272345671', '+233572345671', '+233262345671', @@ -8747,6 +8817,8 @@ describe('Validators', () => { '+260966684590', '+260976684590', '260976684590', + '+260779493521', + '+260760010936', ], invalid: [ '12345', @@ -8754,6 +8826,7 @@ describe('Validators', () => { 'Vml2YW11cyBmZXJtZtesting123', '010-38238383', '966684590', + '760010936', ], }, { @@ -9536,15 +9609,44 @@ describe('Validators', () => { { locale: 'ky-KG', valid: [ - '+7 727 123 4567', - '+7 714 2396102', - '77271234567', - '0271234567', - ], - invalid: [ - '02188565377', - '09386932778', - '0938693277vadggjdsaasdgj8', + '+996553033300', + '+996 222 123456', + '+996 500 987654', + '+996 555 111222', + '+996 700 333444', + '+996 770 555666', + '+996 880 777888', + '+996 990 999000', + '+996 995 555666', + '+996 996 555666', + '+996 997 555666', + '+996 998 555666', + ], + invalid: [ + '+996 201 123456', + '+996 312 123456', + '+996 3960 12345', + '+996 3961 12345', + '+996 3962 12345', + '+996 3963 12345', + '+996 3964 12345', + '+996 3965 12345', + '+996 3966 12345', + '+996 3967 12345', + '+996 3968 12345', + '+996 511 123456', + '+996 522 123456', + '+996 561 123456', + '+996 571 123456', + '+996 624 123456', + '+996 623 123456', + '+996 622 123456', + '+996 609 123456', + '+996 100 12345', + '+996 100 1234567', + '996 100 123456', + '0 100 123456', + '0 100 123abc', ], }, { @@ -12678,6 +12780,9 @@ describe('Validators', () => { '39100-000', '22040-020', '39400-152', + '39100000', + '22040020', + '39400152', ], invalid: [ '79800A12', @@ -13004,6 +13109,55 @@ describe('Validators', () => { }); }); + it('should validate ISO6346 shipping container IDs with checksum digit 10 represented as 0', () => { + test({ + validator: 'isISO6346', + valid: [ + 'APZU3789870', + 'TEMU1002030', + 'DFSU1704420', + 'CMAU2221480', + 'SEGU5060260', + 'FCIU8939320', + 'TRHU3495670', + 'MEDU3871410', + 'CMAU2184010', + 'TCLU2265970', + ], + invalid: [ + 'APZU3789871', // Incorrect check digit + 'TEMU1002031', + 'DFSU1704421', + 'CMAU2221481', + 'SEGU5060261', + ], + }); + }); + it('should validate ISO6346 shipping container IDs with checksum digit 10 represented as 0', () => { + test({ + validator: 'isFreightContainerID', + valid: [ + 'APZU3789870', + 'TEMU1002030', + 'DFSU1704420', + 'CMAU2221480', + 'SEGU5060260', + 'FCIU8939320', + 'TRHU3495670', + 'MEDU3871410', + 'CMAU2184010', + 'TCLU2265970', + ], + invalid: [ + 'APZU3789871', // Incorrect check digit + 'TEMU1002031', + 'DFSU1704421', + 'CMAU2221481', + 'SEGU5060261', + ], + }); + }); + // EU-UK valid numbers sourced from https://ec.europa.eu/taxation_customs/tin/specs/FS-TIN%20Algorithms-Public.docx or constructed by @tplessas. it('should validate taxID', () => { test({ @@ -13939,6 +14093,7 @@ describe('Validators', () => { new Date([2014, 2, 15]), new Date('2014-03-15'), '29.02.2020', + '02.29.2020.20', '2024-', '2024-05', '2024-05-',