string.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. 'use strict';
  2. // Load modules
  3. const Net = require('net');
  4. const Hoek = require('hoek');
  5. const Isemail = require('isemail');
  6. const Any = require('./any');
  7. const Ref = require('./ref');
  8. const JoiDate = require('./date');
  9. const Uri = require('./string/uri');
  10. const Ip = require('./string/ip');
  11. // Declare internals
  12. const internals = {
  13. uriRegex: Uri.createUriRegex(),
  14. ipRegex: Ip.createIpRegex(['ipv4', 'ipv6', 'ipvfuture'], 'optional')
  15. };
  16. internals.String = class extends Any {
  17. constructor() {
  18. super();
  19. this._type = 'string';
  20. this._invalids.add('');
  21. }
  22. _base(value, state, options) {
  23. if (typeof value === 'string' &&
  24. options.convert) {
  25. if (this._flags.case) {
  26. value = (this._flags.case === 'upper' ? value.toLocaleUpperCase() : value.toLocaleLowerCase());
  27. }
  28. if (this._flags.trim) {
  29. value = value.trim();
  30. }
  31. if (this._inner.replacements) {
  32. for (let i = 0; i < this._inner.replacements.length; ++i) {
  33. const replacement = this._inner.replacements[i];
  34. value = value.replace(replacement.pattern, replacement.replacement);
  35. }
  36. }
  37. if (this._flags.truncate) {
  38. for (let i = 0; i < this._tests.length; ++i) {
  39. const test = this._tests[i];
  40. if (test.name === 'max') {
  41. value = value.slice(0, test.arg);
  42. break;
  43. }
  44. }
  45. }
  46. }
  47. return {
  48. value,
  49. errors: (typeof value === 'string') ? null : this.createError('string.base', { value }, state, options)
  50. };
  51. }
  52. insensitive() {
  53. const obj = this.clone();
  54. obj._flags.insensitive = true;
  55. return obj;
  56. }
  57. creditCard() {
  58. return this._test('creditCard', undefined, function (value, state, options) {
  59. let i = value.length;
  60. let sum = 0;
  61. let mul = 1;
  62. while (i--) {
  63. const char = value.charAt(i) * mul;
  64. sum = sum + (char - (char > 9) * 9);
  65. mul = mul ^ 3;
  66. }
  67. const check = (sum % 10 === 0) && (sum > 0);
  68. return check ? value : this.createError('string.creditCard', { value }, state, options);
  69. });
  70. }
  71. regex(pattern, name) {
  72. Hoek.assert(pattern instanceof RegExp, 'pattern must be a RegExp');
  73. pattern = new RegExp(pattern.source, pattern.ignoreCase ? 'i' : undefined); // Future version should break this and forbid unsupported regex flags
  74. return this._test('regex', pattern, function (value, state, options) {
  75. if (pattern.test(value)) {
  76. return value;
  77. }
  78. return this.createError((name ? 'string.regex.name' : 'string.regex.base'), { name, pattern, value }, state, options);
  79. });
  80. }
  81. alphanum() {
  82. return this._test('alphanum', undefined, function (value, state, options) {
  83. if (/^[a-zA-Z0-9]+$/.test(value)) {
  84. return value;
  85. }
  86. return this.createError('string.alphanum', { value }, state, options);
  87. });
  88. }
  89. token() {
  90. return this._test('token', undefined, function (value, state, options) {
  91. if (/^\w+$/.test(value)) {
  92. return value;
  93. }
  94. return this.createError('string.token', { value }, state, options);
  95. });
  96. }
  97. email(isEmailOptions) {
  98. if (isEmailOptions) {
  99. Hoek.assert(typeof isEmailOptions === 'object', 'email options must be an object');
  100. Hoek.assert(typeof isEmailOptions.checkDNS === 'undefined', 'checkDNS option is not supported');
  101. Hoek.assert(typeof isEmailOptions.tldWhitelist === 'undefined' ||
  102. typeof isEmailOptions.tldWhitelist === 'object', 'tldWhitelist must be an array or object');
  103. Hoek.assert(typeof isEmailOptions.minDomainAtoms === 'undefined' ||
  104. Hoek.isInteger(isEmailOptions.minDomainAtoms) && isEmailOptions.minDomainAtoms > 0,
  105. 'minDomainAtoms must be a positive integer');
  106. Hoek.assert(typeof isEmailOptions.errorLevel === 'undefined' || typeof isEmailOptions.errorLevel === 'boolean' ||
  107. (Hoek.isInteger(isEmailOptions.errorLevel) && isEmailOptions.errorLevel >= 0),
  108. 'errorLevel must be a non-negative integer or boolean');
  109. }
  110. return this._test('email', isEmailOptions, function (value, state, options) {
  111. try {
  112. const result = Isemail.validate(value, isEmailOptions);
  113. if (result === true || result === 0) {
  114. return value;
  115. }
  116. }
  117. catch (e) { }
  118. return this.createError('string.email', { value }, state, options);
  119. });
  120. }
  121. ip(ipOptions) {
  122. let regex = internals.ipRegex;
  123. ipOptions = ipOptions || {};
  124. Hoek.assert(typeof ipOptions === 'object', 'options must be an object');
  125. if (ipOptions.cidr) {
  126. Hoek.assert(typeof ipOptions.cidr === 'string', 'cidr must be a string');
  127. ipOptions.cidr = ipOptions.cidr.toLowerCase();
  128. Hoek.assert(ipOptions.cidr in Ip.cidrs, 'cidr must be one of ' + Object.keys(Ip.cidrs).join(', '));
  129. // If we only received a `cidr` setting, create a regex for it. But we don't need to create one if `cidr` is "optional" since that is the default
  130. if (!ipOptions.version && ipOptions.cidr !== 'optional') {
  131. regex = Ip.createIpRegex(['ipv4', 'ipv6', 'ipvfuture'], ipOptions.cidr);
  132. }
  133. }
  134. else {
  135. // Set our default cidr strategy
  136. ipOptions.cidr = 'optional';
  137. }
  138. let versions;
  139. if (ipOptions.version) {
  140. if (!Array.isArray(ipOptions.version)) {
  141. ipOptions.version = [ipOptions.version];
  142. }
  143. Hoek.assert(ipOptions.version.length >= 1, 'version must have at least 1 version specified');
  144. versions = [];
  145. for (let i = 0; i < ipOptions.version.length; ++i) {
  146. let version = ipOptions.version[i];
  147. Hoek.assert(typeof version === 'string', 'version at position ' + i + ' must be a string');
  148. version = version.toLowerCase();
  149. Hoek.assert(Ip.versions[version], 'version at position ' + i + ' must be one of ' + Object.keys(Ip.versions).join(', '));
  150. versions.push(version);
  151. }
  152. // Make sure we have a set of versions
  153. versions = Hoek.unique(versions);
  154. regex = Ip.createIpRegex(versions, ipOptions.cidr);
  155. }
  156. return this._test('ip', ipOptions, function (value, state, options) {
  157. if (regex.test(value)) {
  158. return value;
  159. }
  160. if (versions) {
  161. return this.createError('string.ipVersion', { value, cidr: ipOptions.cidr, version: versions }, state, options);
  162. }
  163. return this.createError('string.ip', { value, cidr: ipOptions.cidr }, state, options);
  164. });
  165. }
  166. uri(uriOptions) {
  167. let customScheme = '';
  168. let allowRelative = false;
  169. let regex = internals.uriRegex;
  170. if (uriOptions) {
  171. Hoek.assert(typeof uriOptions === 'object', 'options must be an object');
  172. if (uriOptions.scheme) {
  173. Hoek.assert(uriOptions.scheme instanceof RegExp || typeof uriOptions.scheme === 'string' || Array.isArray(uriOptions.scheme), 'scheme must be a RegExp, String, or Array');
  174. if (!Array.isArray(uriOptions.scheme)) {
  175. uriOptions.scheme = [uriOptions.scheme];
  176. }
  177. Hoek.assert(uriOptions.scheme.length >= 1, 'scheme must have at least 1 scheme specified');
  178. // Flatten the array into a string to be used to match the schemes.
  179. for (let i = 0; i < uriOptions.scheme.length; ++i) {
  180. const scheme = uriOptions.scheme[i];
  181. Hoek.assert(scheme instanceof RegExp || typeof scheme === 'string', 'scheme at position ' + i + ' must be a RegExp or String');
  182. // Add OR separators if a value already exists
  183. customScheme = customScheme + (customScheme ? '|' : '');
  184. // If someone wants to match HTTP or HTTPS for example then we need to support both RegExp and String so we don't escape their pattern unknowingly.
  185. if (scheme instanceof RegExp) {
  186. customScheme = customScheme + scheme.source;
  187. }
  188. else {
  189. Hoek.assert(/[a-zA-Z][a-zA-Z0-9+-\.]*/.test(scheme), 'scheme at position ' + i + ' must be a valid scheme');
  190. customScheme = customScheme + Hoek.escapeRegex(scheme);
  191. }
  192. }
  193. }
  194. if (uriOptions.allowRelative) {
  195. allowRelative = true;
  196. }
  197. }
  198. if (customScheme || allowRelative) {
  199. regex = Uri.createUriRegex(customScheme, allowRelative);
  200. }
  201. return this._test('uri', uriOptions, function (value, state, options) {
  202. if (regex.test(value)) {
  203. return value;
  204. }
  205. if (customScheme) {
  206. return this.createError('string.uriCustomScheme', { scheme: customScheme, value }, state, options);
  207. }
  208. return this.createError('string.uri', { value }, state, options);
  209. });
  210. }
  211. isoDate() {
  212. return this._test('isoDate', undefined, function (value, state, options) {
  213. if (JoiDate._isIsoDate(value)) {
  214. return value;
  215. }
  216. return this.createError('string.isoDate', { value }, state, options);
  217. });
  218. }
  219. guid(guidOptions) {
  220. const brackets = {
  221. '{': '}', '[': ']', '(': ')', '': ''
  222. };
  223. const uuids = {
  224. 'uuidv1': '1',
  225. 'uuidv2': '2',
  226. 'uuidv3': '3',
  227. 'uuidv4': '4',
  228. 'uuidv5': '5'
  229. };
  230. const versions = [];
  231. if (guidOptions && guidOptions.version) {
  232. if (!Array.isArray(guidOptions.version)) {
  233. guidOptions.version = [guidOptions.version];
  234. }
  235. Hoek.assert(guidOptions.version.length >= 1, 'version must have at least 1 valid version specified');
  236. for (let i = 0; i < guidOptions.version.length; ++i) {
  237. let version = guidOptions.version[i];
  238. Hoek.assert(typeof version === 'string', 'version at position ' + i + ' must be a string');
  239. version = version.toLowerCase();
  240. Hoek.assert(uuids[version], 'version at position ' + i + ' must be one of ' + Object.keys(uuids).join(', '));
  241. Hoek.assert(versions.indexOf(version) === -1, 'version at position ' + i + ' must not be a duplicate.');
  242. versions.push(version);
  243. }
  244. }
  245. const regex = /^([\[{\(]?)([0-9A-F]{8})([:-]?)([0-9A-F]{4})([:-]?)([0-9A-F]{4})([:-]?)([0-9A-F]{4})([:-]?)([0-9A-F]{12})([\]}\)]?)$/i;
  246. return this._test('guid', guidOptions, function (value, state, options) {
  247. const results = regex.exec(value);
  248. if (!results) {
  249. return this.createError('string.guid', { value }, state, options);
  250. }
  251. // Matching braces
  252. if (brackets[results[1]] !== results[11]) {
  253. return this.createError('string.guid', { value }, state, options);
  254. }
  255. // Matching separators
  256. if (results[3] !== results[5] || results[3] !== results[7] || results[3] !== results[9]) {
  257. return this.createError('string.guid', { value }, state, options);
  258. }
  259. // Specific UUID versions
  260. if (versions.length) {
  261. const validVersions = versions.some((uuidVersion) => {
  262. return results[6][0] === uuids[uuidVersion];
  263. });
  264. // Valid version and 89AB check
  265. if (!(validVersions && /[89AB]/i.test(results[8][0]))) {
  266. return this.createError('string.guid', { value }, state, options);
  267. }
  268. }
  269. return value;
  270. });
  271. }
  272. hex() {
  273. const regex = /^[a-f0-9]+$/i;
  274. return this._test('hex', regex, function (value, state, options) {
  275. if (regex.test(value)) {
  276. return value;
  277. }
  278. return this.createError('string.hex', { value }, state, options);
  279. });
  280. }
  281. hostname() {
  282. const regex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
  283. return this._test('hostname', undefined, function (value, state, options) {
  284. if ((value.length <= 255 && regex.test(value)) ||
  285. Net.isIPv6(value)) {
  286. return value;
  287. }
  288. return this.createError('string.hostname', { value }, state, options);
  289. });
  290. }
  291. lowercase() {
  292. const obj = this._test('lowercase', undefined, function (value, state, options) {
  293. if (options.convert ||
  294. value === value.toLocaleLowerCase()) {
  295. return value;
  296. }
  297. return this.createError('string.lowercase', { value }, state, options);
  298. });
  299. obj._flags.case = 'lower';
  300. return obj;
  301. }
  302. uppercase() {
  303. const obj = this._test('uppercase', undefined, function (value, state, options) {
  304. if (options.convert ||
  305. value === value.toLocaleUpperCase()) {
  306. return value;
  307. }
  308. return this.createError('string.uppercase', { value }, state, options);
  309. });
  310. obj._flags.case = 'upper';
  311. return obj;
  312. }
  313. trim() {
  314. const obj = this._test('trim', undefined, function (value, state, options) {
  315. if (options.convert ||
  316. value === value.trim()) {
  317. return value;
  318. }
  319. return this.createError('string.trim', { value }, state, options);
  320. });
  321. obj._flags.trim = true;
  322. return obj;
  323. }
  324. replace(pattern, replacement) {
  325. if (typeof pattern === 'string') {
  326. pattern = new RegExp(Hoek.escapeRegex(pattern), 'g');
  327. }
  328. Hoek.assert(pattern instanceof RegExp, 'pattern must be a RegExp');
  329. Hoek.assert(typeof replacement === 'string', 'replacement must be a String');
  330. // This can not be considere a test like trim, we can't "reject"
  331. // anything from this rule, so just clone the current object
  332. const obj = this.clone();
  333. if (!obj._inner.replacements) {
  334. obj._inner.replacements = [];
  335. }
  336. obj._inner.replacements.push({
  337. pattern,
  338. replacement
  339. });
  340. return obj;
  341. }
  342. truncate(enabled) {
  343. const obj = this.clone();
  344. obj._flags.truncate = enabled === undefined ? true : !!enabled;
  345. return obj;
  346. }
  347. };
  348. internals.compare = function (type, compare) {
  349. return function (limit, encoding) {
  350. const isRef = Ref.isRef(limit);
  351. Hoek.assert((Hoek.isInteger(limit) && limit >= 0) || isRef, 'limit must be a positive integer or reference');
  352. Hoek.assert(!encoding || Buffer.isEncoding(encoding), 'Invalid encoding:', encoding);
  353. return this._test(type, limit, function (value, state, options) {
  354. let compareTo;
  355. if (isRef) {
  356. compareTo = limit(state.parent, options);
  357. if (!Hoek.isInteger(compareTo)) {
  358. return this.createError('string.ref', { ref: limit.key }, state, options);
  359. }
  360. }
  361. else {
  362. compareTo = limit;
  363. }
  364. if (compare(value, compareTo, encoding)) {
  365. return value;
  366. }
  367. return this.createError('string.' + type, { limit: compareTo, value, encoding }, state, options);
  368. });
  369. };
  370. };
  371. internals.String.prototype.min = internals.compare('min', (value, limit, encoding) => {
  372. const length = encoding ? Buffer.byteLength(value, encoding) : value.length;
  373. return length >= limit;
  374. });
  375. internals.String.prototype.max = internals.compare('max', (value, limit, encoding) => {
  376. const length = encoding ? Buffer.byteLength(value, encoding) : value.length;
  377. return length <= limit;
  378. });
  379. internals.String.prototype.length = internals.compare('length', (value, limit, encoding) => {
  380. const length = encoding ? Buffer.byteLength(value, encoding) : value.length;
  381. return length === limit;
  382. });
  383. // Aliases
  384. internals.String.prototype.uuid = internals.String.prototype.guid;
  385. module.exports = new internals.String();