validation.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. 'use strict';
  2. 'require baseclass';
  3. function bytelen(x) {
  4. return new Blob([x]).size;
  5. }
  6. var Validator = baseclass.extend({
  7. __name__: 'Validation',
  8. __init__: function(field, type, optional, vfunc, validatorFactory) {
  9. this.field = field;
  10. this.optional = optional;
  11. this.vfunc = vfunc;
  12. this.vstack = validatorFactory.compile(type);
  13. this.factory = validatorFactory;
  14. },
  15. assert: function(condition, message) {
  16. if (!condition) {
  17. this.field.classList.add('cbi-input-invalid');
  18. this.error = message;
  19. return false;
  20. }
  21. this.field.classList.remove('cbi-input-invalid');
  22. this.error = null;
  23. return true;
  24. },
  25. apply: function(name, value, args) {
  26. var func;
  27. if (typeof(name) === 'function')
  28. func = name;
  29. else if (typeof(this.factory.types[name]) === 'function')
  30. func = this.factory.types[name];
  31. else
  32. return false;
  33. if (value != null)
  34. this.value = value;
  35. return func.apply(this, args);
  36. },
  37. validate: function() {
  38. /* element is detached */
  39. if (!findParent(this.field, 'body') && !findParent(this.field, '[data-field]'))
  40. return true;
  41. this.field.classList.remove('cbi-input-invalid');
  42. this.value = (this.field.value != null) ? this.field.value : '';
  43. this.error = null;
  44. var valid;
  45. if (this.value.length === 0)
  46. valid = this.assert(this.optional, _('non-empty value'));
  47. else
  48. valid = this.vstack[0].apply(this, this.vstack[1]);
  49. if (valid !== true) {
  50. this.field.setAttribute('data-tooltip', _('Expecting: %s').format(this.error));
  51. this.field.setAttribute('data-tooltip-style', 'error');
  52. this.field.dispatchEvent(new CustomEvent('validation-failure', { bubbles: true }));
  53. return false;
  54. }
  55. if (typeof(this.vfunc) == 'function')
  56. valid = this.vfunc(this.value);
  57. if (valid !== true) {
  58. this.assert(false, valid);
  59. this.field.setAttribute('data-tooltip', valid);
  60. this.field.setAttribute('data-tooltip-style', 'error');
  61. this.field.dispatchEvent(new CustomEvent('validation-failure', { bubbles: true }));
  62. return false;
  63. }
  64. this.field.removeAttribute('data-tooltip');
  65. this.field.removeAttribute('data-tooltip-style');
  66. this.field.dispatchEvent(new CustomEvent('validation-success', { bubbles: true }));
  67. return true;
  68. },
  69. });
  70. var ValidatorFactory = baseclass.extend({
  71. __name__: 'ValidatorFactory',
  72. create: function(field, type, optional, vfunc) {
  73. return new Validator(field, type, optional, vfunc, this);
  74. },
  75. compile: function(code) {
  76. var pos = 0;
  77. var esc = false;
  78. var depth = 0;
  79. var stack = [ ];
  80. code += ',';
  81. for (var i = 0; i < code.length; i++) {
  82. if (esc) {
  83. esc = false;
  84. continue;
  85. }
  86. switch (code.charCodeAt(i))
  87. {
  88. case 92:
  89. esc = true;
  90. break;
  91. case 40:
  92. case 44:
  93. if (depth <= 0) {
  94. if (pos < i) {
  95. var label = code.substring(pos, i);
  96. label = label.replace(/\\(.)/g, '$1');
  97. label = label.replace(/^[ \t]+/g, '');
  98. label = label.replace(/[ \t]+$/g, '');
  99. if (label && !isNaN(label)) {
  100. stack.push(parseFloat(label));
  101. }
  102. else if (label.match(/^(['"]).*\1$/)) {
  103. stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
  104. }
  105. else if (typeof this.types[label] == 'function') {
  106. stack.push(this.types[label]);
  107. stack.push(null);
  108. }
  109. else {
  110. L.raise('SyntaxError', 'Unhandled token "%s"', label);
  111. }
  112. }
  113. pos = i+1;
  114. }
  115. depth += (code.charCodeAt(i) == 40);
  116. break;
  117. case 41:
  118. if (--depth <= 0) {
  119. if (typeof stack[stack.length-2] != 'function')
  120. L.raise('SyntaxError', 'Argument list follows non-function');
  121. stack[stack.length-1] = this.compile(code.substring(pos, i));
  122. pos = i+1;
  123. }
  124. break;
  125. }
  126. }
  127. return stack;
  128. },
  129. parseInteger: function(x) {
  130. return (/^-?\d+$/.test(x) ? +x : NaN);
  131. },
  132. parseDecimal: function(x) {
  133. return (/^-?\d+(?:\.\d+)?$/.test(x) ? +x : NaN);
  134. },
  135. parseIPv4: function(x) {
  136. if (!x.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))
  137. return null;
  138. if (RegExp.$1 > 255 || RegExp.$2 > 255 || RegExp.$3 > 255 || RegExp.$4 > 255)
  139. return null;
  140. return [ +RegExp.$1, +RegExp.$2, +RegExp.$3, +RegExp.$4 ];
  141. },
  142. parseIPv6: function(x) {
  143. if (x.match(/^([a-fA-F0-9:]+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)) {
  144. var v6 = RegExp.$1, v4 = this.parseIPv4(RegExp.$2);
  145. if (!v4)
  146. return null;
  147. x = v6 + ':' + (v4[0] * 256 + v4[1]).toString(16)
  148. + ':' + (v4[2] * 256 + v4[3]).toString(16);
  149. }
  150. if (!x.match(/^[a-fA-F0-9:]+$/))
  151. return null;
  152. var prefix_suffix = x.split(/::/);
  153. if (prefix_suffix.length > 2)
  154. return null;
  155. var prefix = (prefix_suffix[0] || '0').split(/:/);
  156. var suffix = prefix_suffix.length > 1 ? (prefix_suffix[1] || '0').split(/:/) : [];
  157. if (suffix.length ? (prefix.length + suffix.length > 7)
  158. : ((prefix_suffix.length < 2 && prefix.length < 8) || prefix.length > 8))
  159. return null;
  160. var i, word;
  161. var words = [];
  162. for (i = 0, word = parseInt(prefix[0], 16); i < prefix.length; word = parseInt(prefix[++i], 16))
  163. if (prefix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
  164. words.push(word);
  165. else
  166. return null;
  167. for (i = 0; i < (8 - prefix.length - suffix.length); i++)
  168. words.push(0);
  169. for (i = 0, word = parseInt(suffix[0], 16); i < suffix.length; word = parseInt(suffix[++i], 16))
  170. if (suffix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
  171. words.push(word);
  172. else
  173. return null;
  174. return words;
  175. },
  176. types: {
  177. integer: function() {
  178. return this.assert(!isNaN(this.factory.parseInteger(this.value)), _('valid integer value'));
  179. },
  180. uinteger: function() {
  181. return this.assert(this.factory.parseInteger(this.value) >= 0, _('positive integer value'));
  182. },
  183. float: function() {
  184. return this.assert(!isNaN(this.factory.parseDecimal(this.value)), _('valid decimal value'));
  185. },
  186. ufloat: function() {
  187. return this.assert(this.factory.parseDecimal(this.value) >= 0, _('positive decimal value'));
  188. },
  189. ipaddr: function(nomask) {
  190. return this.assert(this.apply('ip4addr', null, [nomask]) || this.apply('ip6addr', null, [nomask]),
  191. nomask ? _('valid IP address') : _('valid IP address or prefix'));
  192. },
  193. ip4addr: function(nomask) {
  194. var re = nomask ? /^(\d+\.\d+\.\d+\.\d+)$/ : /^(\d+\.\d+\.\d+\.\d+)(?:\/(\d+\.\d+\.\d+\.\d+)|\/(\d{1,2}))?$/,
  195. m = this.value.match(re);
  196. return this.assert(m && this.factory.parseIPv4(m[1]) && (m[2] ? this.factory.parseIPv4(m[2]) : (m[3] ? this.apply('ip4prefix', m[3]) : true)),
  197. nomask ? _('valid IPv4 address') : _('valid IPv4 address or network'));
  198. },
  199. ip6addr: function(nomask) {
  200. var re = nomask ? /^([0-9a-fA-F:.]+)$/ : /^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/,
  201. m = this.value.match(re);
  202. return this.assert(m && this.factory.parseIPv6(m[1]) && (m[2] ? this.apply('ip6prefix', m[2]) : true),
  203. nomask ? _('valid IPv6 address') : _('valid IPv6 address or prefix'));
  204. },
  205. ip4prefix: function() {
  206. return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 32,
  207. _('valid IPv4 prefix value (0-32)'));
  208. },
  209. ip6prefix: function() {
  210. return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 128,
  211. _('valid IPv6 prefix value (0-128)'));
  212. },
  213. cidr: function(negative) {
  214. return this.assert(this.apply('cidr4', null, [negative]) || this.apply('cidr6', null, [negative]),
  215. _('valid IPv4 or IPv6 CIDR'));
  216. },
  217. cidr4: function(negative) {
  218. var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(-)?(\d{1,2})$/);
  219. return this.assert(m && this.factory.parseIPv4(m[1]) && (negative || !m[2]) && this.apply('ip4prefix', m[3]),
  220. _('valid IPv4 CIDR'));
  221. },
  222. cidr6: function(negative) {
  223. var m = this.value.match(/^([0-9a-fA-F:.]+)\/(-)?(\d{1,3})$/);
  224. return this.assert(m && this.factory.parseIPv6(m[1]) && (negative || !m[2]) && this.apply('ip6prefix', m[3]),
  225. _('valid IPv6 CIDR'));
  226. },
  227. ipnet4: function() {
  228. var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
  229. return this.assert(m && this.factory.parseIPv4(m[1]) && this.factory.parseIPv4(m[2]), _('IPv4 network in address/netmask notation'));
  230. },
  231. ipnet6: function() {
  232. var m = this.value.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/);
  233. return this.assert(m && this.factory.parseIPv6(m[1]) && this.factory.parseIPv6(m[2]), _('IPv6 network in address/netmask notation'));
  234. },
  235. ip6hostid: function() {
  236. if (this.value == "eui64" || this.value == "random")
  237. return true;
  238. var v6 = this.factory.parseIPv6(this.value);
  239. return this.assert(!(!v6 || v6[0] || v6[1] || v6[2] || v6[3]), _('valid IPv6 host id'));
  240. },
  241. ipmask: function(negative) {
  242. return this.assert(this.apply('ipmask4', null, [negative]) || this.apply('ipmask6', null, [negative]),
  243. _('valid network in address/netmask notation'));
  244. },
  245. ipmask4: function(negative) {
  246. return this.assert(this.apply('cidr4', null, [negative]) || this.apply('ipnet4') || this.apply('ip4addr'),
  247. _('valid IPv4 network'));
  248. },
  249. ipmask6: function(negative) {
  250. return this.assert(this.apply('cidr6', null, [negative]) || this.apply('ipnet6') || this.apply('ip6addr'),
  251. _('valid IPv6 network'));
  252. },
  253. port: function() {
  254. var p = this.factory.parseInteger(this.value);
  255. return this.assert(p >= 0 && p <= 65535, _('valid port value'));
  256. },
  257. portrange: function() {
  258. if (this.value.match(/^(\d+)-(\d+)$/)) {
  259. var p1 = +RegExp.$1;
  260. var p2 = +RegExp.$2;
  261. return this.assert(p1 <= p2 && p2 <= 65535,
  262. _('valid port or port range (port1-port2)'));
  263. }
  264. return this.assert(this.apply('port'), _('valid port or port range (port1-port2)'));
  265. },
  266. macaddr: function() {
  267. return this.assert(this.value.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null,
  268. _('valid MAC address'));
  269. },
  270. host: function(ipv4only) {
  271. return this.assert(this.apply('hostname') || this.apply(ipv4only == 1 ? 'ip4addr' : 'ipaddr', null, ['nomask']),
  272. _('valid hostname or IP address'));
  273. },
  274. hostname: function(strict) {
  275. if (this.value.length <= 253)
  276. return this.assert(
  277. (this.value.match(/^[a-zA-Z0-9_]+$/) != null ||
  278. (this.value.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
  279. this.value.match(/[^0-9.]/))) &&
  280. (!strict || !this.value.match(/^_/)),
  281. _('valid hostname'));
  282. return this.assert(false, _('valid hostname'));
  283. },
  284. network: function() {
  285. return this.assert(this.apply('uciname') || this.apply('hostname') || this.apply('ip4addr') || this.apply('ip6addr'),
  286. _('valid UCI identifier, hostname or IP address range'));
  287. },
  288. hostport: function(ipv4only) {
  289. var hp = this.value.split(/:/);
  290. return this.assert(hp.length == 2 && this.apply('host', hp[0], [ipv4only]) && this.apply('port', hp[1]),
  291. _('valid host:port'));
  292. },
  293. ip4addrport: function() {
  294. var hp = this.value.split(/:/);
  295. return this.assert(hp.length == 2 && this.apply('ip4addr', hp[0], [true]) && this.apply('port', hp[1]),
  296. _('valid IPv4 address:port'));
  297. },
  298. ipaddrport: function(bracket) {
  299. var m4 = this.value.match(/^([^\[\]:]+):(\d+)$/),
  300. m6 = this.value.match((bracket == 1) ? /^\[(.+)\]:(\d+)$/ : /^([^\[\]]+):(\d+)$/);
  301. if (m4)
  302. return this.assert(this.apply('ip4addr', m4[1], [true]) && this.apply('port', m4[2]),
  303. _('valid address:port'));
  304. return this.assert(m6 && this.apply('ip6addr', m6[1], [true]) && this.apply('port', m6[2]),
  305. _('valid address:port'));
  306. },
  307. wpakey: function() {
  308. var v = this.value;
  309. if (v.length == 64)
  310. return this.assert(v.match(/^[a-fA-F0-9]{64}$/), _('valid hexadecimal WPA key'));
  311. return this.assert((v.length >= 8) && (v.length <= 63), _('key between 8 and 63 characters'));
  312. },
  313. wepkey: function() {
  314. var v = this.value;
  315. if (v.substr(0, 2) === 's:')
  316. v = v.substr(2);
  317. if ((v.length == 10) || (v.length == 26))
  318. return this.assert(v.match(/^[a-fA-F0-9]{10,26}$/), _('valid hexadecimal WEP key'));
  319. return this.assert((v.length === 5) || (v.length === 13), _('key with either 5 or 13 characters'));
  320. },
  321. uciname: function() {
  322. return this.assert(this.value.match(/^[a-zA-Z0-9_]+$/), _('valid UCI identifier'));
  323. },
  324. range: function(min, max) {
  325. var val = this.factory.parseDecimal(this.value);
  326. return this.assert(val >= +min && val <= +max, _('value between %f and %f').format(min, max));
  327. },
  328. min: function(min) {
  329. return this.assert(this.factory.parseDecimal(this.value) >= +min, _('value greater or equal to %f').format(min));
  330. },
  331. max: function(max) {
  332. return this.assert(this.factory.parseDecimal(this.value) <= +max, _('value smaller or equal to %f').format(max));
  333. },
  334. length: function(len) {
  335. return this.assert(bytelen(this.value) == +len,
  336. _('value with %d characters').format(len));
  337. },
  338. rangelength: function(min, max) {
  339. var len = bytelen(this.value);
  340. return this.assert((len >= +min) && (len <= +max),
  341. _('value between %d and %d characters').format(min, max));
  342. },
  343. minlength: function(min) {
  344. return this.assert(bytelen(this.value) >= +min,
  345. _('value with at least %d characters').format(min));
  346. },
  347. maxlength: function(max) {
  348. return this.assert(bytelen(this.value) <= +max,
  349. _('value with at most %d characters').format(max));
  350. },
  351. or: function() {
  352. var errors = [];
  353. for (var i = 0; i < arguments.length; i += 2) {
  354. if (typeof arguments[i] != 'function') {
  355. if (arguments[i] == this.value)
  356. return this.assert(true);
  357. errors.push('"%s"'.format(arguments[i]));
  358. i--;
  359. }
  360. else if (arguments[i].apply(this, arguments[i+1])) {
  361. return this.assert(true);
  362. }
  363. else {
  364. errors.push(this.error);
  365. }
  366. }
  367. var t = _('One of the following: %s');
  368. return this.assert(false, t.format('\n - ' + errors.join('\n - ')));
  369. },
  370. and: function() {
  371. for (var i = 0; i < arguments.length; i += 2) {
  372. if (typeof arguments[i] != 'function') {
  373. if (arguments[i] != this.value)
  374. return this.assert(false, '"%s"'.format(arguments[i]));
  375. i--;
  376. }
  377. else if (!arguments[i].apply(this, arguments[i+1])) {
  378. return this.assert(false, this.error);
  379. }
  380. }
  381. return this.assert(true);
  382. },
  383. neg: function() {
  384. this.value = this.value.replace(/^[ \t]*![ \t]*/, '');
  385. if (arguments[0].apply(this, arguments[1]))
  386. return this.assert(true);
  387. return this.assert(false, _('Potential negation of: %s').format(this.error));
  388. },
  389. list: function(subvalidator, subargs) {
  390. this.field.setAttribute('data-is-list', 'true');
  391. var tokens = this.value.match(/[^ \t]+/g);
  392. for (var i = 0; i < tokens.length; i++)
  393. if (!this.apply(subvalidator, tokens[i], subargs))
  394. return this.assert(false, this.error);
  395. return this.assert(true);
  396. },
  397. phonedigit: function() {
  398. return this.assert(this.value.match(/^[0-9\*#!\.]+$/),
  399. _('valid phone digit (0-9, "*", "#", "!" or ".")'));
  400. },
  401. timehhmmss: function() {
  402. return this.assert(this.value.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/),
  403. _('valid time (HH:MM:SS)'));
  404. },
  405. dateyyyymmdd: function() {
  406. if (this.value.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) {
  407. var year = +RegExp.$1,
  408. month = +RegExp.$2,
  409. day = +RegExp.$3,
  410. days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
  411. var is_leap_year = function(year) {
  412. return ((!(year % 4) && (year % 100)) || !(year % 400));
  413. }
  414. var get_days_in_month = function(month, year) {
  415. return (month === 2 && is_leap_year(year)) ? 29 : days_in_month[month - 1];
  416. }
  417. /* Firewall rules in the past don't make sense */
  418. return this.assert(year >= 2015 && month && month <= 12 && day && day <= get_days_in_month(month, year),
  419. _('valid date (YYYY-MM-DD)'));
  420. }
  421. return this.assert(false, _('valid date (YYYY-MM-DD)'));
  422. },
  423. unique: function(subvalidator, subargs) {
  424. var ctx = this,
  425. option = findParent(ctx.field, '[data-widget][data-name]'),
  426. section = findParent(option, '.cbi-section'),
  427. query = '[data-widget="%s"][data-name="%s"]'.format(option.getAttribute('data-widget'), option.getAttribute('data-name')),
  428. unique = true;
  429. section.querySelectorAll(query).forEach(function(sibling) {
  430. if (sibling === option)
  431. return;
  432. var input = sibling.querySelector('[data-type]'),
  433. values = input ? (input.getAttribute('data-is-list') ? input.value.match(/[^ \t]+/g) : [ input.value ]) : null;
  434. if (values !== null && values.indexOf(ctx.value) !== -1)
  435. unique = false;
  436. });
  437. if (!unique)
  438. return this.assert(false, _('unique value'));
  439. if (typeof(subvalidator) === 'function')
  440. return this.apply(subvalidator, null, subargs);
  441. return this.assert(true);
  442. },
  443. hexstring: function() {
  444. return this.assert(this.value.match(/^([a-f0-9][a-f0-9]|[A-F0-9][A-F0-9])+$/),
  445. _('hexadecimal encoded value'));
  446. },
  447. string: function() {
  448. return true;
  449. }
  450. }
  451. });
  452. return ValidatorFactory;