luci2: add L.toColor(), L.parseIPv4(), L.parseIPv6() and L.isNetmask() helper
[project/luci2/ui.git] / luci2 / htdocs / luci2 / luci2.js
1 /*
2 LuCI2 - OpenWrt Web Interface
3
4 Copyright 2013 Jo-Philipp Wich <jow@openwrt.org>
5
6 Licensed under the Apache License, Version 2.0 (the "License");
7 you may not use this file except in compliance with the License.
8 You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11 */
12
13 String.prototype.format = function()
14 {
15 var html_esc = [/&/g, '&#38;', /"/g, '&#34;', /'/g, '&#39;', /</g, '&#60;', />/g, '&#62;'];
16 var quot_esc = [/"/g, '&#34;', /'/g, '&#39;'];
17
18 function esc(s, r) {
19 for( var i = 0; i < r.length; i += 2 )
20 s = s.replace(r[i], r[i+1]);
21 return s;
22 }
23
24 var str = this;
25 var out = '';
26 var re = /^(([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X|q|h|j|t|m))/;
27 var a = b = [], numSubstitutions = 0, numMatches = 0;
28
29 while ((a = re.exec(str)) != null)
30 {
31 var m = a[1];
32 var leftpart = a[2], pPad = a[3], pJustify = a[4], pMinLength = a[5];
33 var pPrecision = a[6], pType = a[7];
34
35 numMatches++;
36
37 if (pType == '%')
38 {
39 subst = '%';
40 }
41 else
42 {
43 if (numSubstitutions < arguments.length)
44 {
45 var param = arguments[numSubstitutions++];
46
47 var pad = '';
48 if (pPad && pPad.substr(0,1) == "'")
49 pad = leftpart.substr(1,1);
50 else if (pPad)
51 pad = pPad;
52
53 var justifyRight = true;
54 if (pJustify && pJustify === "-")
55 justifyRight = false;
56
57 var minLength = -1;
58 if (pMinLength)
59 minLength = parseInt(pMinLength);
60
61 var precision = -1;
62 if (pPrecision && pType == 'f')
63 precision = parseInt(pPrecision.substring(1));
64
65 var subst = param;
66
67 switch(pType)
68 {
69 case 'b':
70 subst = (parseInt(param) || 0).toString(2);
71 break;
72
73 case 'c':
74 subst = String.fromCharCode(parseInt(param) || 0);
75 break;
76
77 case 'd':
78 subst = (parseInt(param) || 0);
79 break;
80
81 case 'u':
82 subst = Math.abs(parseInt(param) || 0);
83 break;
84
85 case 'f':
86 subst = (precision > -1)
87 ? ((parseFloat(param) || 0.0)).toFixed(precision)
88 : (parseFloat(param) || 0.0);
89 break;
90
91 case 'o':
92 subst = (parseInt(param) || 0).toString(8);
93 break;
94
95 case 's':
96 subst = param;
97 break;
98
99 case 'x':
100 subst = ('' + (parseInt(param) || 0).toString(16)).toLowerCase();
101 break;
102
103 case 'X':
104 subst = ('' + (parseInt(param) || 0).toString(16)).toUpperCase();
105 break;
106
107 case 'h':
108 subst = esc(param, html_esc);
109 break;
110
111 case 'q':
112 subst = esc(param, quot_esc);
113 break;
114
115 case 'j':
116 subst = String.serialize(param);
117 break;
118
119 case 't':
120 var td = 0;
121 var th = 0;
122 var tm = 0;
123 var ts = (param || 0);
124
125 if (ts > 60) {
126 tm = Math.floor(ts / 60);
127 ts = (ts % 60);
128 }
129
130 if (tm > 60) {
131 th = Math.floor(tm / 60);
132 tm = (tm % 60);
133 }
134
135 if (th > 24) {
136 td = Math.floor(th / 24);
137 th = (th % 24);
138 }
139
140 subst = (td > 0)
141 ? '%dd %dh %dm %ds'.format(td, th, tm, ts)
142 : '%dh %dm %ds'.format(th, tm, ts);
143
144 break;
145
146 case 'm':
147 var mf = pMinLength ? parseInt(pMinLength) : 1000;
148 var pr = pPrecision ? Math.floor(10*parseFloat('0'+pPrecision)) : 2;
149
150 var i = 0;
151 var val = parseFloat(param || 0);
152 var units = [ '', 'K', 'M', 'G', 'T', 'P', 'E' ];
153
154 for (i = 0; (i < units.length) && (val > mf); i++)
155 val /= mf;
156
157 subst = val.toFixed(pr) + ' ' + units[i];
158 break;
159 }
160
161 subst = (typeof(subst) == 'undefined') ? '' : subst.toString();
162
163 if (minLength > 0 && pad.length > 0)
164 for (var i = 0; i < (minLength - subst.length); i++)
165 subst = justifyRight ? (pad + subst) : (subst + pad);
166 }
167 }
168
169 out += leftpart + subst;
170 str = str.substr(m.length);
171 }
172
173 return out + str;
174 }
175
176 function LuCI2()
177 {
178 var L = this;
179
180 var Class = function() { };
181
182 Class.extend = function(properties)
183 {
184 Class.initializing = true;
185
186 var prototype = new this();
187 var superprot = this.prototype;
188
189 Class.initializing = false;
190
191 $.extend(prototype, properties, {
192 callSuper: function() {
193 var args = [ ];
194 var meth = arguments[0];
195
196 if (typeof(superprot[meth]) != 'function')
197 return undefined;
198
199 for (var i = 1; i < arguments.length; i++)
200 args.push(arguments[i]);
201
202 return superprot[meth].apply(this, args);
203 }
204 });
205
206 function _class()
207 {
208 this.options = arguments[0] || { };
209
210 if (!Class.initializing && typeof(this.init) == 'function')
211 this.init.apply(this, arguments);
212 }
213
214 _class.prototype = prototype;
215 _class.prototype.constructor = _class;
216
217 _class.extend = Class.extend;
218
219 return _class;
220 };
221
222 this.defaults = function(obj, def)
223 {
224 for (var key in def)
225 if (typeof(obj[key]) == 'undefined')
226 obj[key] = def[key];
227
228 return obj;
229 };
230
231 this.isDeferred = function(x)
232 {
233 return (typeof(x) == 'object' &&
234 typeof(x.then) == 'function' &&
235 typeof(x.promise) == 'function');
236 };
237
238 this.deferrable = function()
239 {
240 if (this.isDeferred(arguments[0]))
241 return arguments[0];
242
243 var d = $.Deferred();
244 d.resolve.apply(d, arguments);
245
246 return d.promise();
247 };
248
249 this.i18n = {
250
251 loaded: false,
252 catalog: { },
253 plural: function(n) { return 0 + (n != 1) },
254
255 init: function() {
256 if (L.i18n.loaded)
257 return;
258
259 var lang = (navigator.userLanguage || navigator.language || 'en').toLowerCase();
260 var langs = (lang.indexOf('-') > -1) ? [ lang, lang.split(/-/)[0] ] : [ lang ];
261
262 for (var i = 0; i < langs.length; i++)
263 $.ajax('%s/i18n/base.%s.json'.format(L.globals.resource, langs[i]), {
264 async: false,
265 cache: true,
266 dataType: 'json',
267 success: function(data) {
268 $.extend(L.i18n.catalog, data);
269
270 var pe = L.i18n.catalog[''];
271 if (pe)
272 {
273 delete L.i18n.catalog[''];
274 try {
275 var pf = new Function('n', 'return 0 + (' + pe + ')');
276 L.i18n.plural = pf;
277 } catch (e) { };
278 }
279 }
280 });
281
282 L.i18n.loaded = true;
283 }
284
285 };
286
287 this.tr = function(msgid)
288 {
289 L.i18n.init();
290
291 var msgstr = L.i18n.catalog[msgid];
292
293 if (typeof(msgstr) == 'undefined')
294 return msgid;
295 else if (typeof(msgstr) == 'string')
296 return msgstr;
297 else
298 return msgstr[0];
299 };
300
301 this.trp = function(msgid, msgid_plural, count)
302 {
303 L.i18n.init();
304
305 var msgstr = L.i18n.catalog[msgid];
306
307 if (typeof(msgstr) == 'undefined')
308 return (count == 1) ? msgid : msgid_plural;
309 else if (typeof(msgstr) == 'string')
310 return msgstr;
311 else
312 return msgstr[L.i18n.plural(count)];
313 };
314
315 this.trc = function(msgctx, msgid)
316 {
317 L.i18n.init();
318
319 var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
320
321 if (typeof(msgstr) == 'undefined')
322 return msgid;
323 else if (typeof(msgstr) == 'string')
324 return msgstr;
325 else
326 return msgstr[0];
327 };
328
329 this.trcp = function(msgctx, msgid, msgid_plural, count)
330 {
331 L.i18n.init();
332
333 var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
334
335 if (typeof(msgstr) == 'undefined')
336 return (count == 1) ? msgid : msgid_plural;
337 else if (typeof(msgstr) == 'string')
338 return msgstr;
339 else
340 return msgstr[L.i18n.plural(count)];
341 };
342
343 this.setHash = function(key, value)
344 {
345 var h = '';
346 var data = this.getHash(undefined);
347
348 if (typeof(value) == 'undefined')
349 delete data[key];
350 else
351 data[key] = value;
352
353 var keys = [ ];
354 for (var k in data)
355 keys.push(k);
356
357 keys.sort();
358
359 for (var i = 0; i < keys.length; i++)
360 {
361 if (i > 0)
362 h += ',';
363
364 h += keys[i] + ':' + data[keys[i]];
365 }
366
367 if (h.length)
368 location.hash = '#' + h;
369 else
370 location.hash = '';
371 };
372
373 this.getHash = function(key)
374 {
375 var data = { };
376 var tuples = (location.hash || '#').substring(1).split(/,/);
377
378 for (var i = 0; i < tuples.length; i++)
379 {
380 var tuple = tuples[i].split(/:/);
381 if (tuple.length == 2)
382 data[tuple[0]] = tuple[1];
383 }
384
385 if (typeof(key) != 'undefined')
386 return data[key];
387
388 return data;
389 };
390
391 this.toArray = function(x)
392 {
393 switch (typeof(x))
394 {
395 case 'number':
396 case 'boolean':
397 return [ x ];
398
399 case 'string':
400 var r = [ ];
401 var l = x.split(/\s+/);
402 for (var i = 0; i < l.length; i++)
403 if (l[i].length > 0)
404 r.push(l[i]);
405 return r;
406
407 case 'object':
408 if ($.isArray(x))
409 {
410 var r = [ ];
411 for (var i = 0; i < x.length; i++)
412 r.push(x[i]);
413 return r;
414 }
415 else if ($.isPlainObject(x))
416 {
417 var r = [ ];
418 for (var k in x)
419 if (x.hasOwnProperty(k))
420 r.push(k);
421 return r.sort();
422 }
423 }
424
425 return [ ];
426 };
427
428 this.toObject = function(x)
429 {
430 switch (typeof(x))
431 {
432 case 'number':
433 case 'boolean':
434 return { x: true };
435
436 case 'string':
437 var r = { };
438 var l = x.split(/\x+/);
439 for (var i = 0; i < l.length; i++)
440 if (l[i].length > 0)
441 r[l[i]] = true;
442 return r;
443
444 case 'object':
445 if ($.isArray(x))
446 {
447 var r = { };
448 for (var i = 0; i < x.length; i++)
449 r[x[i]] = true;
450 return r;
451 }
452 else if ($.isPlainObject(x))
453 {
454 return x;
455 }
456 }
457
458 return { };
459 };
460
461 this.filterArray = function(array, item)
462 {
463 if (!$.isArray(array))
464 return [ ];
465
466 for (var i = 0; i < array.length; i++)
467 if (array[i] === item)
468 array.splice(i--, 1);
469
470 return array;
471 };
472
473 this.toClassName = function(str, suffix)
474 {
475 var n = '';
476 var l = str.split(/[\/.]/);
477
478 for (var i = 0; i < l.length; i++)
479 if (l[i].length > 0)
480 n += l[i].charAt(0).toUpperCase() + l[i].substr(1).toLowerCase();
481
482 if (typeof(suffix) == 'string')
483 n += suffix;
484
485 return n;
486 };
487
488 this.toColor = function(str)
489 {
490 if (typeof(str) != 'string' || str.length == 0)
491 return '#CCCCCC';
492
493 if (str == 'wan')
494 return '#F09090';
495 else if (str == 'lan')
496 return '#90F090';
497
498 var i = 0, hash = 0;
499
500 while (i < str.length)
501 hash = str.charCodeAt(i++) + ((hash << 5) - hash);
502
503 var r = (hash & 0xFF) % 128;
504 var g = ((hash >> 8) & 0xFF) % 128;
505
506 var min = 0;
507 var max = 128;
508
509 if ((r + g) < 128)
510 min = 128 - r - g;
511 else
512 max = 255 - r - g;
513
514 var b = min + (((hash >> 16) & 0xFF) % (max - min));
515
516 return '#%02X%02X%02X'.format(0xFF - r, 0xFF - g, 0xFF - b);
517 };
518
519 this.parseIPv4 = function(str)
520 {
521 if ((typeof(str) != 'string' && !(str instanceof String)) ||
522 !str.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
523 return undefined;
524
525 var num = [ ];
526 var parts = str.split(/\./);
527
528 for (var i = 0; i < parts.length; i++)
529 {
530 var n = parseInt(parts[i], 10);
531 if (isNaN(n) || n > 255)
532 return undefined;
533
534 num.push(n);
535 }
536
537 return num;
538 };
539
540 this.parseIPv6 = function(str)
541 {
542 if ((typeof(str) != 'string' && !(str instanceof String)) ||
543 !str.match(/^[a-fA-F0-9:]+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/))
544 return undefined;
545
546 var parts = str.split(/::/);
547 if (parts.length == 0 || parts.length > 2)
548 return undefined;
549
550 var lnum = [ ];
551 if (parts[0].length > 0)
552 {
553 var left = parts[0].split(/:/);
554 for (var i = 0; i < left.length; i++)
555 {
556 var n = parseInt(left[i], 16);
557 if (isNaN(n))
558 return undefined;
559
560 lnum.push((n / 256) >> 0);
561 lnum.push(n % 256);
562 }
563 }
564
565 var rnum = [ ];
566 if (parts.length > 1 && parts[1].length > 0)
567 {
568 var right = parts[1].split(/:/);
569
570 for (var i = 0; i < right.length; i++)
571 {
572 if (right[i].indexOf('.') > 0)
573 {
574 var addr = L.parseIPv4(right[i]);
575 if (!addr)
576 return undefined;
577
578 rnum.push.apply(rnum, addr);
579 continue;
580 }
581
582 var n = parseInt(right[i], 16);
583 if (isNaN(n))
584 return undefined;
585
586 rnum.push((n / 256) >> 0);
587 rnum.push(n % 256);
588 }
589 }
590
591 if (rnum.length > 0 && (lnum.length + rnum.length) > 15)
592 return undefined;
593
594 var num = [ ];
595
596 num.push.apply(num, lnum);
597
598 for (var i = 0; i < (16 - lnum.length - rnum.length); i++)
599 num.push(0);
600
601 num.push.apply(num, rnum);
602
603 if (num.length > 16)
604 return undefined;
605
606 return num;
607 };
608
609 this.isNetmask = function(addr)
610 {
611 if (!$.isArray(addr))
612 return false;
613
614 var c;
615
616 for (c = 0; (c < addr.length) && (addr[c] == 255); c++);
617
618 if (c == addr.length)
619 return true;
620
621 if ((addr[c] == 254) || (addr[c] == 252) || (addr[c] == 248) ||
622 (addr[c] == 240) || (addr[c] == 224) || (addr[c] == 192) ||
623 (addr[c] == 128) || (addr[c] == 0))
624 {
625 for (c++; (c < addr.length) && (addr[c] == 0); c++);
626
627 if (c == addr.length)
628 return true;
629 }
630
631 return false;
632 };
633
634 this.globals = {
635 timeout: 15000,
636 resource: '/luci2',
637 sid: '00000000000000000000000000000000'
638 };
639
640 this.rpc = {
641
642 _id: 1,
643 _batch: undefined,
644 _requests: { },
645
646 _call: function(req, cb)
647 {
648 return $.ajax('/ubus', {
649 cache: false,
650 contentType: 'application/json',
651 data: JSON.stringify(req),
652 dataType: 'json',
653 type: 'POST',
654 timeout: L.globals.timeout,
655 _rpc_req: req
656 }).then(cb, cb);
657 },
658
659 _list_cb: function(msg)
660 {
661 var list = msg.result;
662
663 /* verify message frame */
664 if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list))
665 list = [ ];
666
667 return $.Deferred().resolveWith(this, [ list ]);
668 },
669
670 _call_cb: function(msg)
671 {
672 var data = [ ];
673 var type = Object.prototype.toString;
674 var reqs = this._rpc_req;
675
676 if (!$.isArray(reqs))
677 {
678 msg = [ msg ];
679 reqs = [ reqs ];
680 }
681
682 for (var i = 0; i < msg.length; i++)
683 {
684 /* fetch related request info */
685 var req = L.rpc._requests[reqs[i].id];
686 if (typeof(req) != 'object')
687 throw 'No related request for JSON response';
688
689 /* fetch response attribute and verify returned type */
690 var ret = undefined;
691
692 /* verify message frame */
693 if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0')
694 if ($.isArray(msg[i].result) && msg[i].result[0] == 0)
695 ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0];
696
697 if (req.expect)
698 {
699 for (var key in req.expect)
700 {
701 if (typeof(ret) != 'undefined' && key != '')
702 ret = ret[key];
703
704 if (typeof(ret) == 'undefined' || type.call(ret) != type.call(req.expect[key]))
705 ret = req.expect[key];
706
707 break;
708 }
709 }
710
711 /* apply filter */
712 if (typeof(req.filter) == 'function')
713 {
714 req.priv[0] = ret;
715 req.priv[1] = req.params;
716 ret = req.filter.apply(L.rpc, req.priv);
717 }
718
719 /* store response data */
720 if (typeof(req.index) == 'number')
721 data[req.index] = ret;
722 else
723 data = ret;
724
725 /* delete request object */
726 delete L.rpc._requests[reqs[i].id];
727 }
728
729 return $.Deferred().resolveWith(this, [ data ]);
730 },
731
732 list: function()
733 {
734 var params = [ ];
735 for (var i = 0; i < arguments.length; i++)
736 params[i] = arguments[i];
737
738 var msg = {
739 jsonrpc: '2.0',
740 id: this._id++,
741 method: 'list',
742 params: (params.length > 0) ? params : undefined
743 };
744
745 return this._call(msg, this._list_cb);
746 },
747
748 batch: function()
749 {
750 if (!$.isArray(this._batch))
751 this._batch = [ ];
752 },
753
754 flush: function()
755 {
756 if (!$.isArray(this._batch))
757 return L.deferrable([ ]);
758
759 var req = this._batch;
760 delete this._batch;
761
762 /* call rpc */
763 return this._call(req, this._call_cb);
764 },
765
766 declare: function(options)
767 {
768 var _rpc = this;
769
770 return function() {
771 /* build parameter object */
772 var p_off = 0;
773 var params = { };
774 if ($.isArray(options.params))
775 for (p_off = 0; p_off < options.params.length; p_off++)
776 params[options.params[p_off]] = arguments[p_off];
777
778 /* all remaining arguments are private args */
779 var priv = [ undefined, undefined ];
780 for (; p_off < arguments.length; p_off++)
781 priv.push(arguments[p_off]);
782
783 /* store request info */
784 var req = _rpc._requests[_rpc._id] = {
785 expect: options.expect,
786 filter: options.filter,
787 params: params,
788 priv: priv
789 };
790
791 /* build message object */
792 var msg = {
793 jsonrpc: '2.0',
794 id: _rpc._id++,
795 method: 'call',
796 params: [
797 L.globals.sid,
798 options.object,
799 options.method,
800 params
801 ]
802 };
803
804 /* when a batch is in progress then store index in request data
805 * and push message object onto the stack */
806 if ($.isArray(_rpc._batch))
807 {
808 req.index = _rpc._batch.push(msg) - 1;
809 return L.deferrable(msg);
810 }
811
812 /* call rpc */
813 return _rpc._call(msg, _rpc._call_cb);
814 };
815 }
816 };
817
818 this.UCIContext = Class.extend({
819
820 init: function()
821 {
822 this.state = {
823 newidx: 0,
824 values: { },
825 creates: { },
826 changes: { },
827 deletes: { },
828 reorder: { }
829 };
830 },
831
832 _load: L.rpc.declare({
833 object: 'uci',
834 method: 'get',
835 params: [ 'config' ],
836 expect: { values: { } }
837 }),
838
839 _order: L.rpc.declare({
840 object: 'uci',
841 method: 'order',
842 params: [ 'config', 'sections' ]
843 }),
844
845 _add: L.rpc.declare({
846 object: 'uci',
847 method: 'add',
848 params: [ 'config', 'type', 'name', 'values' ],
849 expect: { section: '' }
850 }),
851
852 _set: L.rpc.declare({
853 object: 'uci',
854 method: 'set',
855 params: [ 'config', 'section', 'values' ]
856 }),
857
858 _delete: L.rpc.declare({
859 object: 'uci',
860 method: 'delete',
861 params: [ 'config', 'section', 'options' ]
862 }),
863
864 _newid: function(conf)
865 {
866 var v = this.state.values;
867 var n = this.state.creates;
868 var sid;
869
870 do {
871 sid = "new%06x".format(Math.random() * 0xFFFFFF);
872 } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
873
874 return sid;
875 },
876
877 load: function(packages)
878 {
879 var self = this;
880 var seen = { };
881 var pkgs = [ ];
882
883 if (!$.isArray(packages))
884 packages = [ packages ];
885
886 L.rpc.batch();
887
888 for (var i = 0; i < packages.length; i++)
889 if (!seen[packages[i]] && !self.state.values[packages[i]])
890 {
891 pkgs.push(packages[i]);
892 seen[packages[i]] = true;
893 self._load(packages[i]);
894 }
895
896 return L.rpc.flush().then(function(responses) {
897 for (var i = 0; i < responses.length; i++)
898 self.state.values[pkgs[i]] = responses[i];
899
900 return pkgs;
901 });
902 },
903
904 unload: function(packages)
905 {
906 if (!$.isArray(packages))
907 packages = [ packages ];
908
909 for (var i = 0; i < packages.length; i++)
910 {
911 delete this.state.values[packages[i]];
912 delete this.state.creates[packages[i]];
913 delete this.state.changes[packages[i]];
914 delete this.state.deletes[packages[i]];
915 }
916 },
917
918 add: function(conf, type, name)
919 {
920 var n = this.state.creates;
921 var sid = this._newid(conf);
922
923 if (!n[conf])
924 n[conf] = { };
925
926 n[conf][sid] = {
927 '.type': type,
928 '.name': sid,
929 '.create': name,
930 '.anonymous': !name,
931 '.index': 1000 + this.state.newidx++
932 };
933
934 return sid;
935 },
936
937 remove: function(conf, sid)
938 {
939 var n = this.state.creates;
940 var c = this.state.changes;
941 var d = this.state.deletes;
942
943 /* requested deletion of a just created section */
944 if (n[conf] && n[conf][sid])
945 {
946 delete n[conf][sid];
947 }
948 else
949 {
950 if (c[conf])
951 delete c[conf][sid];
952
953 if (!d[conf])
954 d[conf] = { };
955
956 d[conf][sid] = true;
957 }
958 },
959
960 sections: function(conf, type, cb)
961 {
962 var sa = [ ];
963 var v = this.state.values[conf];
964 var n = this.state.creates[conf];
965 var c = this.state.changes[conf];
966 var d = this.state.deletes[conf];
967
968 if (!v)
969 return sa;
970
971 for (var s in v)
972 if (!d || d[s] !== true)
973 if (!type || v[s]['.type'] == type)
974 sa.push($.extend({ }, v[s], c ? c[s] : undefined));
975
976 if (n)
977 for (var s in n)
978 if (!type || n[s]['.type'] == type)
979 sa.push(n[s]);
980
981 sa.sort(function(a, b) {
982 return a['.index'] - b['.index'];
983 });
984
985 for (var i = 0; i < sa.length; i++)
986 sa[i]['.index'] = i;
987
988 if (typeof(cb) == 'function')
989 for (var i = 0; i < sa.length; i++)
990 cb.call(this, sa[i], sa[i]['.name']);
991
992 return sa;
993 },
994
995 get: function(conf, sid, opt)
996 {
997 var v = this.state.values;
998 var n = this.state.creates;
999 var c = this.state.changes;
1000 var d = this.state.deletes;
1001
1002 if (typeof(sid) == 'undefined')
1003 return undefined;
1004
1005 /* requested option in a just created section */
1006 if (n[conf] && n[conf][sid])
1007 {
1008 if (!n[conf])
1009 return undefined;
1010
1011 if (typeof(opt) == 'undefined')
1012 return n[conf][sid];
1013
1014 return n[conf][sid][opt];
1015 }
1016
1017 /* requested an option value */
1018 if (typeof(opt) != 'undefined')
1019 {
1020 /* check whether option was deleted */
1021 if (d[conf] && d[conf][sid])
1022 {
1023 if (d[conf][sid] === true)
1024 return undefined;
1025
1026 for (var i = 0; i < d[conf][sid].length; i++)
1027 if (d[conf][sid][i] == opt)
1028 return undefined;
1029 }
1030
1031 /* check whether option was changed */
1032 if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined')
1033 return c[conf][sid][opt];
1034
1035 /* return base value */
1036 if (v[conf] && v[conf][sid])
1037 return v[conf][sid][opt];
1038
1039 return undefined;
1040 }
1041
1042 /* requested an entire section */
1043 if (v[conf])
1044 return v[conf][sid];
1045
1046 return undefined;
1047 },
1048
1049 set: function(conf, sid, opt, val)
1050 {
1051 var v = this.state.values;
1052 var n = this.state.creates;
1053 var c = this.state.changes;
1054 var d = this.state.deletes;
1055
1056 if (typeof(sid) == 'undefined' ||
1057 typeof(opt) == 'undefined' ||
1058 opt.charAt(0) == '.')
1059 return;
1060
1061 if (n[conf] && n[conf][sid])
1062 {
1063 if (typeof(val) != 'undefined')
1064 n[conf][sid][opt] = val;
1065 else
1066 delete n[conf][sid][opt];
1067 }
1068 else if (typeof(val) != 'undefined')
1069 {
1070 /* do not set within deleted section */
1071 if (d[conf] && d[conf][sid] === true)
1072 return;
1073
1074 /* only set in existing sections */
1075 if (!v[conf] || !v[conf][sid])
1076 return;
1077
1078 if (!c[conf])
1079 c[conf] = { };
1080
1081 if (!c[conf][sid])
1082 c[conf][sid] = { };
1083
1084 /* undelete option */
1085 if (d[conf] && d[conf][sid])
1086 d[conf][sid] = L.filterArray(d[conf][sid], opt);
1087
1088 c[conf][sid][opt] = val;
1089 }
1090 else
1091 {
1092 /* only delete in existing sections */
1093 if (!v[conf] || !v[conf][sid])
1094 return;
1095
1096 if (!d[conf])
1097 d[conf] = { };
1098
1099 if (!d[conf][sid])
1100 d[conf][sid] = [ ];
1101
1102 if (d[conf][sid] !== true)
1103 d[conf][sid].push(opt);
1104 }
1105 },
1106
1107 unset: function(conf, sid, opt)
1108 {
1109 return this.set(conf, sid, opt, undefined);
1110 },
1111
1112 _reload: function()
1113 {
1114 var pkgs = [ ];
1115
1116 for (var pkg in this.state.values)
1117 pkgs.push(pkg);
1118
1119 this.init();
1120
1121 return this.load(pkgs);
1122 },
1123
1124 _reorder: function()
1125 {
1126 var v = this.state.values;
1127 var n = this.state.creates;
1128 var r = this.state.reorder;
1129
1130 if ($.isEmptyObject(r))
1131 return L.deferrable();
1132
1133 L.rpc.batch();
1134
1135 /*
1136 gather all created and existing sections, sort them according
1137 to their index value and issue an uci order call
1138 */
1139 for (var c in r)
1140 {
1141 var o = [ ];
1142
1143 if (n[c])
1144 for (var s in n[c])
1145 o.push(n[c][s]);
1146
1147 for (var s in v[c])
1148 o.push(v[c][s]);
1149
1150 if (o.length > 0)
1151 {
1152 o.sort(function(a, b) {
1153 return (a['.index'] - b['.index']);
1154 });
1155
1156 var sids = [ ];
1157
1158 for (var i = 0; i < o.length; i++)
1159 sids.push(o[i]['.name']);
1160
1161 this._order(c, sids);
1162 }
1163 }
1164
1165 this.state.reorder = { };
1166 return L.rpc.flush();
1167 },
1168
1169 swap: function(conf, sid1, sid2)
1170 {
1171 var s1 = this.get(conf, sid1);
1172 var s2 = this.get(conf, sid2);
1173 var n1 = s1 ? s1['.index'] : NaN;
1174 var n2 = s2 ? s2['.index'] : NaN;
1175
1176 if (isNaN(n1) || isNaN(n2))
1177 return false;
1178
1179 s1['.index'] = n2;
1180 s2['.index'] = n1;
1181
1182 this.state.reorder[conf] = true;
1183
1184 return true;
1185 },
1186
1187 save: function()
1188 {
1189 L.rpc.batch();
1190
1191 var v = this.state.values;
1192 var n = this.state.creates;
1193 var c = this.state.changes;
1194 var d = this.state.deletes;
1195
1196 var self = this;
1197 var snew = [ ];
1198 var pkgs = { };
1199
1200 if (n)
1201 for (var conf in n)
1202 {
1203 for (var sid in n[conf])
1204 {
1205 var r = {
1206 config: conf,
1207 values: { }
1208 };
1209
1210 for (var k in n[conf][sid])
1211 {
1212 if (k == '.type')
1213 r.type = n[conf][sid][k];
1214 else if (k == '.create')
1215 r.name = n[conf][sid][k];
1216 else if (k.charAt(0) != '.')
1217 r.values[k] = n[conf][sid][k];
1218 }
1219
1220 snew.push(n[conf][sid]);
1221
1222 self._add(r.config, r.type, r.name, r.values);
1223 }
1224
1225 pkgs[conf] = true;
1226 }
1227
1228 if (c)
1229 for (var conf in c)
1230 {
1231 for (var sid in c[conf])
1232 self._set(conf, sid, c[conf][sid]);
1233
1234 pkgs[conf] = true;
1235 }
1236
1237 if (d)
1238 for (var conf in d)
1239 {
1240 for (var sid in d[conf])
1241 {
1242 var o = d[conf][sid];
1243 self._delete(conf, sid, (o === true) ? undefined : o);
1244 }
1245
1246 pkgs[conf] = true;
1247 }
1248
1249 return L.rpc.flush().then(function(responses) {
1250 /*
1251 array "snew" holds references to the created uci sections,
1252 use it to assign the returned names of the new sections
1253 */
1254 for (var i = 0; i < snew.length; i++)
1255 snew[i]['.name'] = responses[i];
1256
1257 return self._reorder();
1258 }).then(function() {
1259 pkgs = L.toArray(pkgs);
1260
1261 self.unload(pkgs);
1262
1263 return self.load(pkgs);
1264 });
1265 },
1266
1267 _apply: L.rpc.declare({
1268 object: 'uci',
1269 method: 'apply',
1270 params: [ 'timeout', 'rollback' ]
1271 }),
1272
1273 _confirm: L.rpc.declare({
1274 object: 'uci',
1275 method: 'confirm'
1276 }),
1277
1278 apply: function(timeout)
1279 {
1280 var self = this;
1281 var date = new Date();
1282 var deferred = $.Deferred();
1283
1284 if (typeof(timeout) != 'number' || timeout < 1)
1285 timeout = 10;
1286
1287 self._apply(timeout, true).then(function(rv) {
1288 if (rv != 0)
1289 {
1290 deferred.rejectWith(self, [ rv ]);
1291 return;
1292 }
1293
1294 var try_deadline = date.getTime() + 1000 * timeout;
1295 var try_confirm = function()
1296 {
1297 return self._confirm().then(function(rv) {
1298 if (rv != 0)
1299 {
1300 if (date.getTime() < try_deadline)
1301 window.setTimeout(try_confirm, 250);
1302 else
1303 deferred.rejectWith(self, [ rv ]);
1304
1305 return;
1306 }
1307
1308 deferred.resolveWith(self, [ rv ]);
1309 });
1310 };
1311
1312 window.setTimeout(try_confirm, 1000);
1313 });
1314
1315 return deferred;
1316 },
1317
1318 changes: L.rpc.declare({
1319 object: 'uci',
1320 method: 'changes',
1321 expect: { changes: { } }
1322 }),
1323
1324 readable: function(conf)
1325 {
1326 return L.session.hasACL('uci', conf, 'read');
1327 },
1328
1329 writable: function(conf)
1330 {
1331 return L.session.hasACL('uci', conf, 'write');
1332 }
1333 });
1334
1335 this.uci = new this.UCIContext();
1336
1337 this.wireless = {
1338 listDeviceNames: L.rpc.declare({
1339 object: 'iwinfo',
1340 method: 'devices',
1341 expect: { 'devices': [ ] },
1342 filter: function(data) {
1343 data.sort();
1344 return data;
1345 }
1346 }),
1347
1348 getDeviceStatus: L.rpc.declare({
1349 object: 'iwinfo',
1350 method: 'info',
1351 params: [ 'device' ],
1352 expect: { '': { } },
1353 filter: function(data, params) {
1354 if (!$.isEmptyObject(data))
1355 {
1356 data['device'] = params['device'];
1357 return data;
1358 }
1359 return undefined;
1360 }
1361 }),
1362
1363 getAssocList: L.rpc.declare({
1364 object: 'iwinfo',
1365 method: 'assoclist',
1366 params: [ 'device' ],
1367 expect: { results: [ ] },
1368 filter: function(data, params) {
1369 for (var i = 0; i < data.length; i++)
1370 data[i]['device'] = params['device'];
1371
1372 data.sort(function(a, b) {
1373 if (a.bssid < b.bssid)
1374 return -1;
1375 else if (a.bssid > b.bssid)
1376 return 1;
1377 else
1378 return 0;
1379 });
1380
1381 return data;
1382 }
1383 }),
1384
1385 getWirelessStatus: function() {
1386 return this.listDeviceNames().then(function(names) {
1387 L.rpc.batch();
1388
1389 for (var i = 0; i < names.length; i++)
1390 L.wireless.getDeviceStatus(names[i]);
1391
1392 return L.rpc.flush();
1393 }).then(function(networks) {
1394 var rv = { };
1395
1396 var phy_attrs = [
1397 'country', 'channel', 'frequency', 'frequency_offset',
1398 'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy'
1399 ];
1400
1401 var net_attrs = [
1402 'ssid', 'bssid', 'mode', 'quality', 'quality_max',
1403 'signal', 'noise', 'bitrate', 'encryption'
1404 ];
1405
1406 for (var i = 0; i < networks.length; i++)
1407 {
1408 var phy = rv[networks[i].phy] || (
1409 rv[networks[i].phy] = { networks: [ ] }
1410 );
1411
1412 var net = {
1413 device: networks[i].device
1414 };
1415
1416 for (var j = 0; j < phy_attrs.length; j++)
1417 phy[phy_attrs[j]] = networks[i][phy_attrs[j]];
1418
1419 for (var j = 0; j < net_attrs.length; j++)
1420 net[net_attrs[j]] = networks[i][net_attrs[j]];
1421
1422 phy.networks.push(net);
1423 }
1424
1425 return rv;
1426 });
1427 },
1428
1429 getAssocLists: function()
1430 {
1431 return this.listDeviceNames().then(function(names) {
1432 L.rpc.batch();
1433
1434 for (var i = 0; i < names.length; i++)
1435 L.wireless.getAssocList(names[i]);
1436
1437 return L.rpc.flush();
1438 }).then(function(assoclists) {
1439 var rv = [ ];
1440
1441 for (var i = 0; i < assoclists.length; i++)
1442 for (var j = 0; j < assoclists[i].length; j++)
1443 rv.push(assoclists[i][j]);
1444
1445 return rv;
1446 });
1447 },
1448
1449 formatEncryption: function(enc)
1450 {
1451 var format_list = function(l, s)
1452 {
1453 var rv = [ ];
1454 for (var i = 0; i < l.length; i++)
1455 rv.push(l[i].toUpperCase());
1456 return rv.join(s ? s : ', ');
1457 }
1458
1459 if (!enc || !enc.enabled)
1460 return L.tr('None');
1461
1462 if (enc.wep)
1463 {
1464 if (enc.wep.length == 2)
1465 return L.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', '));
1466 else if (enc.wep[0] == 'shared')
1467 return L.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', '));
1468 else
1469 return L.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', '));
1470 }
1471 else if (enc.wpa)
1472 {
1473 if (enc.wpa.length == 2)
1474 return L.tr('mixed WPA/WPA2') + ' %s (%s)'.format(
1475 format_list(enc.authentication, '/'),
1476 format_list(enc.ciphers, ', ')
1477 );
1478 else if (enc.wpa[0] == 2)
1479 return 'WPA2 %s (%s)'.format(
1480 format_list(enc.authentication, '/'),
1481 format_list(enc.ciphers, ', ')
1482 );
1483 else
1484 return 'WPA %s (%s)'.format(
1485 format_list(enc.authentication, '/'),
1486 format_list(enc.ciphers, ', ')
1487 );
1488 }
1489
1490 return L.tr('Unknown');
1491 }
1492 };
1493
1494 this.firewall = {
1495 getZoneColor: function(zone)
1496 {
1497 if ($.isPlainObject(zone))
1498 zone = zone.name;
1499
1500 if (zone == 'lan')
1501 return '#90f090';
1502 else if (zone == 'wan')
1503 return '#f09090';
1504
1505 for (var i = 0, hash = 0;
1506 i < zone.length;
1507 hash = zone.charCodeAt(i++) + ((hash << 5) - hash));
1508
1509 for (var i = 0, color = '#';
1510 i < 3;
1511 color += ('00' + ((hash >> i++ * 8) & 0xFF).tostring(16)).slice(-2));
1512
1513 return color;
1514 },
1515
1516 findZoneByNetwork: function(network)
1517 {
1518 var self = this;
1519 var zone = undefined;
1520
1521 return L.uci.sections('firewall', 'zone', function(z) {
1522 if (!z.name || !z.network)
1523 return;
1524
1525 if (!$.isArray(z.network))
1526 z.network = z.network.split(/\s+/);
1527
1528 for (var i = 0; i < z.network.length; i++)
1529 {
1530 if (z.network[i] == network)
1531 {
1532 zone = z;
1533 break;
1534 }
1535 }
1536 }).then(function() {
1537 if (zone)
1538 zone.color = self.getZoneColor(zone);
1539
1540 return zone;
1541 });
1542 }
1543 };
1544
1545 this.NetworkModel = {
1546 _device_blacklist: [
1547 /^gre[0-9]+$/,
1548 /^gretap[0-9]+$/,
1549 /^ifb[0-9]+$/,
1550 /^ip6tnl[0-9]+$/,
1551 /^sit[0-9]+$/,
1552 /^wlan[0-9]+\.sta[0-9]+$/
1553 ],
1554
1555 _cache_functions: [
1556 'protolist', 0, L.rpc.declare({
1557 object: 'network',
1558 method: 'get_proto_handlers',
1559 expect: { '': { } }
1560 }),
1561 'ifstate', 1, L.rpc.declare({
1562 object: 'network.interface',
1563 method: 'dump',
1564 expect: { 'interface': [ ] }
1565 }),
1566 'devstate', 2, L.rpc.declare({
1567 object: 'network.device',
1568 method: 'status',
1569 expect: { '': { } }
1570 }),
1571 'wifistate', 0, L.rpc.declare({
1572 object: 'network.wireless',
1573 method: 'status',
1574 expect: { '': { } }
1575 }),
1576 'bwstate', 2, L.rpc.declare({
1577 object: 'luci2.network.bwmon',
1578 method: 'statistics',
1579 expect: { 'statistics': { } }
1580 }),
1581 'devlist', 2, L.rpc.declare({
1582 object: 'luci2.network',
1583 method: 'device_list',
1584 expect: { 'devices': [ ] }
1585 }),
1586 'swlist', 0, L.rpc.declare({
1587 object: 'luci2.network',
1588 method: 'switch_list',
1589 expect: { 'switches': [ ] }
1590 })
1591 ],
1592
1593 _fetch_protocol: function(proto)
1594 {
1595 var url = L.globals.resource + '/proto/' + proto + '.js';
1596 var self = L.NetworkModel;
1597
1598 var def = $.Deferred();
1599
1600 $.ajax(url, {
1601 method: 'GET',
1602 cache: true,
1603 dataType: 'text'
1604 }).then(function(data) {
1605 try {
1606 var protoConstructorSource = (
1607 '(function(L, $) { ' +
1608 'return %s' +
1609 '})(L, $);\n\n' +
1610 '//@ sourceURL=%s'
1611 ).format(data, url);
1612
1613 var protoClass = eval(protoConstructorSource);
1614
1615 self._protos[proto] = new protoClass();
1616 }
1617 catch(e) {
1618 alert('Unable to instantiate proto "%s": %s'.format(url, e));
1619 };
1620
1621 def.resolve();
1622 }).fail(function() {
1623 def.resolve();
1624 });
1625
1626 return def;
1627 },
1628
1629 _fetch_protocols: function()
1630 {
1631 var self = L.NetworkModel;
1632 var deferreds = [
1633 self._fetch_protocol('none')
1634 ];
1635
1636 for (var proto in self._cache.protolist)
1637 deferreds.push(self._fetch_protocol(proto));
1638
1639 return $.when.apply($, deferreds);
1640 },
1641
1642 _fetch_swstate: L.rpc.declare({
1643 object: 'luci2.network',
1644 method: 'switch_info',
1645 params: [ 'switch' ],
1646 expect: { 'info': { } }
1647 }),
1648
1649 _fetch_swstate_cb: function(responses) {
1650 var self = L.NetworkModel;
1651 var swlist = self._cache.swlist;
1652 var swstate = self._cache.swstate = { };
1653
1654 for (var i = 0; i < responses.length; i++)
1655 swstate[swlist[i]] = responses[i];
1656 },
1657
1658 _fetch_cache_cb: function(level)
1659 {
1660 var self = L.NetworkModel;
1661 var name = '_fetch_cache_cb_' + level;
1662
1663 return self[name] || (
1664 self[name] = function(responses)
1665 {
1666 for (var i = 0; i < self._cache_functions.length; i += 3)
1667 if (!level || self._cache_functions[i + 1] == level)
1668 self._cache[self._cache_functions[i]] = responses.shift();
1669
1670 if (!level)
1671 {
1672 L.rpc.batch();
1673
1674 for (var i = 0; i < self._cache.swlist.length; i++)
1675 self._fetch_swstate(self._cache.swlist[i]);
1676
1677 return L.rpc.flush().then(self._fetch_swstate_cb);
1678 }
1679
1680 return L.deferrable();
1681 }
1682 );
1683 },
1684
1685 _fetch_cache: function(level)
1686 {
1687 var self = L.NetworkModel;
1688
1689 return L.uci.load(['network', 'wireless']).then(function() {
1690 L.rpc.batch();
1691
1692 for (var i = 0; i < self._cache_functions.length; i += 3)
1693 if (!level || self._cache_functions[i + 1] == level)
1694 self._cache_functions[i + 2]();
1695
1696 return L.rpc.flush().then(self._fetch_cache_cb(level || 0));
1697 });
1698 },
1699
1700 _get: function(pkg, sid, key)
1701 {
1702 return L.uci.get(pkg, sid, key);
1703 },
1704
1705 _set: function(pkg, sid, key, val)
1706 {
1707 return L.uci.set(pkg, sid, key, val);
1708 },
1709
1710 _is_blacklisted: function(dev)
1711 {
1712 for (var i = 0; i < this._device_blacklist.length; i++)
1713 if (dev.match(this._device_blacklist[i]))
1714 return true;
1715
1716 return false;
1717 },
1718
1719 _sort_devices: function(a, b)
1720 {
1721 if (a.options.kind < b.options.kind)
1722 return -1;
1723 else if (a.options.kind > b.options.kind)
1724 return 1;
1725
1726 if (a.options.name < b.options.name)
1727 return -1;
1728 else if (a.options.name > b.options.name)
1729 return 1;
1730
1731 return 0;
1732 },
1733
1734 _get_dev: function(ifname)
1735 {
1736 var alias = (ifname.charAt(0) == '@');
1737 return this._devs[ifname] || (
1738 this._devs[ifname] = {
1739 ifname: ifname,
1740 kind: alias ? 'alias' : 'ethernet',
1741 type: alias ? 0 : 1,
1742 up: false,
1743 changed: { }
1744 }
1745 );
1746 },
1747
1748 _get_iface: function(name)
1749 {
1750 return this._ifaces[name] || (
1751 this._ifaces[name] = {
1752 name: name,
1753 proto: this._protos.none,
1754 changed: { }
1755 }
1756 );
1757 },
1758
1759 _parse_devices: function()
1760 {
1761 var self = L.NetworkModel;
1762 var wificount = { };
1763
1764 for (var ifname in self._cache.devstate)
1765 {
1766 if (self._is_blacklisted(ifname))
1767 continue;
1768
1769 var dev = self._cache.devstate[ifname];
1770 var entry = self._get_dev(ifname);
1771
1772 entry.up = dev.up;
1773
1774 switch (dev.type)
1775 {
1776 case 'IP tunnel':
1777 entry.kind = 'tunnel';
1778 break;
1779
1780 case 'Bridge':
1781 entry.kind = 'bridge';
1782 //entry.ports = dev['bridge-members'].sort();
1783 break;
1784 }
1785 }
1786
1787 for (var i = 0; i < self._cache.devlist.length; i++)
1788 {
1789 var dev = self._cache.devlist[i];
1790
1791 if (self._is_blacklisted(dev.device))
1792 continue;
1793
1794 var entry = self._get_dev(dev.device);
1795
1796 entry.up = dev.is_up;
1797 entry.type = dev.type;
1798
1799 switch (dev.type)
1800 {
1801 case 1: /* Ethernet */
1802 if (dev.is_bridge)
1803 entry.kind = 'bridge';
1804 else if (dev.is_tuntap)
1805 entry.kind = 'tunnel';
1806 else if (dev.is_wireless)
1807 entry.kind = 'wifi';
1808 break;
1809
1810 case 512: /* PPP */
1811 case 768: /* IP-IP Tunnel */
1812 case 769: /* IP6-IP6 Tunnel */
1813 case 776: /* IPv6-in-IPv4 */
1814 case 778: /* GRE over IP */
1815 entry.kind = 'tunnel';
1816 break;
1817 }
1818 }
1819
1820 var net = L.uci.sections('network');
1821 for (var i = 0; i < net.length; i++)
1822 {
1823 var s = net[i];
1824 var sid = s['.name'];
1825
1826 if (s['.type'] == 'device' && s.name)
1827 {
1828 var entry = self._get_dev(s.name);
1829
1830 switch (s.type)
1831 {
1832 case 'macvlan':
1833 case 'tunnel':
1834 entry.kind = 'tunnel';
1835 break;
1836 }
1837
1838 entry.sid = sid;
1839 }
1840 else if (s['.type'] == 'interface' && !s['.anonymous'] && s.ifname)
1841 {
1842 var ifnames = L.toArray(s.ifname);
1843
1844 for (var j = 0; j < ifnames.length; j++)
1845 self._get_dev(ifnames[j]);
1846
1847 if (s['.name'] != 'loopback')
1848 {
1849 var entry = self._get_dev('@%s'.format(s['.name']));
1850
1851 entry.type = 0;
1852 entry.kind = 'alias';
1853 entry.sid = sid;
1854 }
1855 }
1856 else if (s['.type'] == 'switch_vlan' && s.device)
1857 {
1858 var sw = self._cache.swstate[s.device];
1859 var vid = parseInt(s.vid || s.vlan);
1860 var ports = L.toArray(s.ports);
1861
1862 if (!sw || !ports.length || isNaN(vid))
1863 continue;
1864
1865 var ifname = undefined;
1866
1867 for (var j = 0; j < ports.length; j++)
1868 {
1869 var port = parseInt(ports[j]);
1870 var tag = (ports[j].replace(/[^tu]/g, '') == 't');
1871
1872 if (port == sw.cpu_port)
1873 {
1874 // XXX: need a way to map switch to netdev
1875 if (tag)
1876 ifname = 'eth0.%d'.format(vid);
1877 else
1878 ifname = 'eth0';
1879
1880 break;
1881 }
1882 }
1883
1884 if (!ifname)
1885 continue;
1886
1887 var entry = self._get_dev(ifname);
1888
1889 entry.kind = 'vlan';
1890 entry.sid = sid;
1891 entry.vsw = sw;
1892 entry.vid = vid;
1893 }
1894 }
1895
1896 var wifi = L.uci.sections('wireless');
1897 for (var i = 0; i < wifi.length; i++)
1898 {
1899 var s = wifi[i];
1900 var sid = s['.name'];
1901
1902 if (s['.type'] == 'wifi-iface' && s.device)
1903 {
1904 var r = parseInt(s.device.replace(/^[^0-9]+/, ''));
1905 var n = wificount[s.device] = (wificount[s.device] || 0) + 1;
1906 var id = 'radio%d.network%d'.format(r, n);
1907 var ifname = id;
1908
1909 if (self._cache.wifistate[s.device])
1910 {
1911 var ifcs = self._cache.wifistate[s.device].interfaces;
1912 for (var ifc in ifcs)
1913 {
1914 if (ifcs[ifc].section == sid)
1915 {
1916 ifname = ifcs[ifc].ifname;
1917 break;
1918 }
1919 }
1920 }
1921
1922 var entry = self._get_dev(ifname);
1923
1924 entry.kind = 'wifi';
1925 entry.sid = sid;
1926 entry.wid = id;
1927 entry.wdev = s.device;
1928 entry.wmode = s.mode;
1929 entry.wssid = s.ssid;
1930 entry.wbssid = s.bssid;
1931 }
1932 }
1933
1934 for (var i = 0; i < net.length; i++)
1935 {
1936 var s = net[i];
1937 var sid = s['.name'];
1938
1939 if (s['.type'] == 'interface' && !s['.anonymous'] && s.type == 'bridge')
1940 {
1941 var ifnames = L.toArray(s.ifname);
1942
1943 for (var ifname in self._devs)
1944 {
1945 var dev = self._devs[ifname];
1946
1947 if (dev.kind != 'wifi')
1948 continue;
1949
1950 var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network'));
1951 if ($.inArray(sid, wnets) > -1)
1952 ifnames.push(ifname);
1953 }
1954
1955 entry = self._get_dev('br-%s'.format(s['.name']));
1956 entry.type = 1;
1957 entry.kind = 'bridge';
1958 entry.sid = sid;
1959 entry.ports = ifnames.sort();
1960 }
1961 }
1962 },
1963
1964 _parse_interfaces: function()
1965 {
1966 var self = L.NetworkModel;
1967 var net = L.uci.sections('network');
1968
1969 for (var i = 0; i < net.length; i++)
1970 {
1971 var s = net[i];
1972 var sid = s['.name'];
1973
1974 if (s['.type'] == 'interface' && !s['.anonymous'] && s.proto)
1975 {
1976 var entry = self._get_iface(s['.name']);
1977 var proto = self._protos[s.proto] || self._protos.none;
1978
1979 var l3dev = undefined;
1980 var l2dev = undefined;
1981
1982 var ifnames = L.toArray(s.ifname);
1983
1984 for (var ifname in self._devs)
1985 {
1986 var dev = self._devs[ifname];
1987
1988 if (dev.kind != 'wifi')
1989 continue;
1990
1991 var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network'));
1992 if ($.inArray(entry.name, wnets) > -1)
1993 ifnames.push(ifname);
1994 }
1995
1996 if (proto.virtual)
1997 l3dev = '%s-%s'.format(s.proto, entry.name);
1998 else if (s.type == 'bridge')
1999 l3dev = 'br-%s'.format(entry.name);
2000 else
2001 l3dev = ifnames[0];
2002
2003 if (!proto.virtual && s.type == 'bridge')
2004 l2dev = 'br-%s'.format(entry.name);
2005 else if (!proto.virtual)
2006 l2dev = ifnames[0];
2007
2008 entry.proto = proto;
2009 entry.sid = sid;
2010 entry.l3dev = l3dev;
2011 entry.l2dev = l2dev;
2012 }
2013 }
2014
2015 for (var i = 0; i < self._cache.ifstate.length; i++)
2016 {
2017 var iface = self._cache.ifstate[i];
2018 var entry = self._get_iface(iface['interface']);
2019 var proto = self._protos[iface.proto] || self._protos.none;
2020
2021 /* this is a virtual interface, either deleted from config but
2022 not applied yet or set up from external tools (6rd) */
2023 if (!entry.sid)
2024 {
2025 entry.proto = proto;
2026 entry.l2dev = iface.device;
2027 entry.l3dev = iface.l3_device;
2028 }
2029 }
2030 },
2031
2032 init: function()
2033 {
2034 var self = this;
2035
2036 if (self._cache)
2037 return L.deferrable();
2038
2039 self._cache = { };
2040 self._devs = { };
2041 self._ifaces = { };
2042 self._protos = { };
2043
2044 return self._fetch_cache()
2045 .then(self._fetch_protocols)
2046 .then(self._parse_devices)
2047 .then(self._parse_interfaces);
2048 },
2049
2050 update: function()
2051 {
2052 delete this._cache;
2053 return this.init();
2054 },
2055
2056 refreshInterfaceStatus: function()
2057 {
2058 return this._fetch_cache(1).then(this._parse_interfaces);
2059 },
2060
2061 refreshDeviceStatus: function()
2062 {
2063 return this._fetch_cache(2).then(this._parse_devices);
2064 },
2065
2066 refreshStatus: function()
2067 {
2068 return this._fetch_cache(1)
2069 .then(this._fetch_cache(2))
2070 .then(this._parse_devices)
2071 .then(this._parse_interfaces);
2072 },
2073
2074 getDevices: function()
2075 {
2076 var devs = [ ];
2077
2078 for (var ifname in this._devs)
2079 if (ifname != 'lo')
2080 devs.push(new L.NetworkModel.Device(this._devs[ifname]));
2081
2082 return devs.sort(this._sort_devices);
2083 },
2084
2085 getDeviceByInterface: function(iface)
2086 {
2087 if (iface instanceof L.NetworkModel.Interface)
2088 iface = iface.name();
2089
2090 if (this._ifaces[iface])
2091 return this.getDevice(this._ifaces[iface].l3dev) ||
2092 this.getDevice(this._ifaces[iface].l2dev);
2093
2094 return undefined;
2095 },
2096
2097 getDevice: function(ifname)
2098 {
2099 if (this._devs[ifname])
2100 return new L.NetworkModel.Device(this._devs[ifname]);
2101
2102 return undefined;
2103 },
2104
2105 createDevice: function(name)
2106 {
2107 return new L.NetworkModel.Device(this._get_dev(name));
2108 },
2109
2110 getInterfaces: function()
2111 {
2112 var ifaces = [ ];
2113
2114 for (var name in this._ifaces)
2115 if (name != 'loopback')
2116 ifaces.push(this.getInterface(name));
2117
2118 ifaces.sort(function(a, b) {
2119 if (a.name() < b.name())
2120 return -1;
2121 else if (a.name() > b.name())
2122 return 1;
2123 else
2124 return 0;
2125 });
2126
2127 return ifaces;
2128 },
2129
2130 getInterfacesByDevice: function(dev)
2131 {
2132 var ifaces = [ ];
2133
2134 if (dev instanceof L.NetworkModel.Device)
2135 dev = dev.name();
2136
2137 for (var name in this._ifaces)
2138 {
2139 var iface = this._ifaces[name];
2140 if (iface.l2dev == dev || iface.l3dev == dev)
2141 ifaces.push(this.getInterface(name));
2142 }
2143
2144 ifaces.sort(function(a, b) {
2145 if (a.name() < b.name())
2146 return -1;
2147 else if (a.name() > b.name())
2148 return 1;
2149 else
2150 return 0;
2151 });
2152
2153 return ifaces;
2154 },
2155
2156 getInterface: function(iface)
2157 {
2158 if (this._ifaces[iface])
2159 return new L.NetworkModel.Interface(this._ifaces[iface]);
2160
2161 return undefined;
2162 },
2163
2164 getProtocols: function()
2165 {
2166 var rv = [ ];
2167
2168 for (var proto in this._protos)
2169 {
2170 var pr = this._protos[proto];
2171
2172 rv.push({
2173 name: proto,
2174 description: pr.description,
2175 virtual: pr.virtual,
2176 tunnel: pr.tunnel
2177 });
2178 }
2179
2180 return rv.sort(function(a, b) {
2181 if (a.name < b.name)
2182 return -1;
2183 else if (a.name > b.name)
2184 return 1;
2185 else
2186 return 0;
2187 });
2188 },
2189
2190 _find_wan: function(ipaddr)
2191 {
2192 for (var i = 0; i < this._cache.ifstate.length; i++)
2193 {
2194 var ifstate = this._cache.ifstate[i];
2195
2196 if (!ifstate.route)
2197 continue;
2198
2199 for (var j = 0; j < ifstate.route.length; j++)
2200 if (ifstate.route[j].mask == 0 &&
2201 ifstate.route[j].target == ipaddr &&
2202 typeof(ifstate.route[j].table) == 'undefined')
2203 {
2204 return this.getInterface(ifstate['interface']);
2205 }
2206 }
2207
2208 return undefined;
2209 },
2210
2211 findWAN: function()
2212 {
2213 return this._find_wan('0.0.0.0');
2214 },
2215
2216 findWAN6: function()
2217 {
2218 return this._find_wan('::');
2219 },
2220
2221 resolveAlias: function(ifname)
2222 {
2223 if (ifname instanceof L.NetworkModel.Device)
2224 ifname = ifname.name();
2225
2226 var dev = this._devs[ifname];
2227 var seen = { };
2228
2229 while (dev && dev.kind == 'alias')
2230 {
2231 // loop
2232 if (seen[dev.ifname])
2233 return undefined;
2234
2235 var ifc = this._ifaces[dev.sid];
2236
2237 seen[dev.ifname] = true;
2238 dev = ifc ? this._devs[ifc.l3dev] : undefined;
2239 }
2240
2241 return dev ? this.getDevice(dev.ifname) : undefined;
2242 }
2243 };
2244
2245 this.NetworkModel.Device = Class.extend({
2246 _wifi_modes: {
2247 ap: L.tr('Master'),
2248 sta: L.tr('Client'),
2249 adhoc: L.tr('Ad-Hoc'),
2250 monitor: L.tr('Monitor'),
2251 wds: L.tr('Static WDS')
2252 },
2253
2254 _status: function(key)
2255 {
2256 var s = L.NetworkModel._cache.devstate[this.options.ifname];
2257
2258 if (s)
2259 return key ? s[key] : s;
2260
2261 return undefined;
2262 },
2263
2264 get: function(key)
2265 {
2266 var sid = this.options.sid;
2267 var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
2268 return L.NetworkModel._get(pkg, sid, key);
2269 },
2270
2271 set: function(key, val)
2272 {
2273 var sid = this.options.sid;
2274 var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
2275 return L.NetworkModel._set(pkg, sid, key, val);
2276 },
2277
2278 init: function()
2279 {
2280 if (typeof(this.options.type) == 'undefined')
2281 this.options.type = 1;
2282
2283 if (typeof(this.options.kind) == 'undefined')
2284 this.options.kind = 'ethernet';
2285
2286 if (typeof(this.options.networks) == 'undefined')
2287 this.options.networks = [ ];
2288 },
2289
2290 name: function()
2291 {
2292 return this.options.ifname;
2293 },
2294
2295 description: function()
2296 {
2297 switch (this.options.kind)
2298 {
2299 case 'alias':
2300 return L.tr('Alias for network "%s"').format(this.options.ifname.substring(1));
2301
2302 case 'bridge':
2303 return L.tr('Network bridge');
2304
2305 case 'ethernet':
2306 return L.tr('Network device');
2307
2308 case 'tunnel':
2309 switch (this.options.type)
2310 {
2311 case 1: /* tuntap */
2312 return L.tr('TAP device');
2313
2314 case 512: /* PPP */
2315 return L.tr('PPP tunnel');
2316
2317 case 768: /* IP-IP Tunnel */
2318 return L.tr('IP-in-IP tunnel');
2319
2320 case 769: /* IP6-IP6 Tunnel */
2321 return L.tr('IPv6-in-IPv6 tunnel');
2322
2323 case 776: /* IPv6-in-IPv4 */
2324 return L.tr('IPv6-over-IPv4 tunnel');
2325 break;
2326
2327 case 778: /* GRE over IP */
2328 return L.tr('GRE-over-IP tunnel');
2329
2330 default:
2331 return L.tr('Tunnel device');
2332 }
2333
2334 case 'vlan':
2335 return L.tr('VLAN %d on %s').format(this.options.vid, this.options.vsw.model);
2336
2337 case 'wifi':
2338 var o = this.options;
2339 return L.trc('(Wifi-Mode) "(SSID)" on (radioX)', '%s "%h" on %s').format(
2340 o.wmode ? this._wifi_modes[o.wmode] : L.tr('Unknown mode'),
2341 o.wssid || '?', o.wdev
2342 );
2343 }
2344
2345 return L.tr('Unknown device');
2346 },
2347
2348 icon: function(up)
2349 {
2350 var kind = this.options.kind;
2351
2352 if (kind == 'alias')
2353 kind = 'ethernet';
2354
2355 if (typeof(up) == 'undefined')
2356 up = this.isUp();
2357
2358 return L.globals.resource + '/icons/%s%s.png'.format(kind, up ? '' : '_disabled');
2359 },
2360
2361 isUp: function()
2362 {
2363 var l = L.NetworkModel._cache.devlist;
2364
2365 for (var i = 0; i < l.length; i++)
2366 if (l[i].device == this.options.ifname)
2367 return (l[i].is_up === true);
2368
2369 return false;
2370 },
2371
2372 isAlias: function()
2373 {
2374 return (this.options.kind == 'alias');
2375 },
2376
2377 isBridge: function()
2378 {
2379 return (this.options.kind == 'bridge');
2380 },
2381
2382 isBridgeable: function()
2383 {
2384 return (this.options.type == 1 && this.options.kind != 'bridge');
2385 },
2386
2387 isWireless: function()
2388 {
2389 return (this.options.kind == 'wifi');
2390 },
2391
2392 isInNetwork: function(net)
2393 {
2394 if (!(net instanceof L.NetworkModel.Interface))
2395 net = L.NetworkModel.getInterface(net);
2396
2397 if (net)
2398 {
2399 if (net.options.l3dev == this.options.ifname ||
2400 net.options.l2dev == this.options.ifname)
2401 return true;
2402
2403 var dev = L.NetworkModel._devs[net.options.l2dev];
2404 if (dev && dev.kind == 'bridge' && dev.ports)
2405 return ($.inArray(this.options.ifname, dev.ports) > -1);
2406 }
2407
2408 return false;
2409 },
2410
2411 getMTU: function()
2412 {
2413 var dev = L.NetworkModel._cache.devstate[this.options.ifname];
2414 if (dev && !isNaN(dev.mtu))
2415 return dev.mtu;
2416
2417 return undefined;
2418 },
2419
2420 getMACAddress: function()
2421 {
2422 if (this.options.type != 1)
2423 return undefined;
2424
2425 var dev = L.NetworkModel._cache.devstate[this.options.ifname];
2426 if (dev && dev.macaddr)
2427 return dev.macaddr.toUpperCase();
2428
2429 return undefined;
2430 },
2431
2432 getInterfaces: function()
2433 {
2434 return L.NetworkModel.getInterfacesByDevice(this.options.name);
2435 },
2436
2437 getStatistics: function()
2438 {
2439 var s = this._status('statistics') || { };
2440 return {
2441 rx_bytes: (s.rx_bytes || 0),
2442 tx_bytes: (s.tx_bytes || 0),
2443 rx_packets: (s.rx_packets || 0),
2444 tx_packets: (s.tx_packets || 0)
2445 };
2446 },
2447
2448 getTrafficHistory: function()
2449 {
2450 var def = new Array(120);
2451
2452 for (var i = 0; i < 120; i++)
2453 def[i] = 0;
2454
2455 var h = L.NetworkModel._cache.bwstate[this.options.ifname] || { };
2456 return {
2457 rx_bytes: (h.rx_bytes || def),
2458 tx_bytes: (h.tx_bytes || def),
2459 rx_packets: (h.rx_packets || def),
2460 tx_packets: (h.tx_packets || def)
2461 };
2462 },
2463
2464 removeFromInterface: function(iface)
2465 {
2466 if (!(iface instanceof L.NetworkModel.Interface))
2467 iface = L.NetworkModel.getInterface(iface);
2468
2469 if (!iface)
2470 return;
2471
2472 var ifnames = L.toArray(iface.get('ifname'));
2473 if ($.inArray(this.options.ifname, ifnames) > -1)
2474 iface.set('ifname', L.filterArray(ifnames, this.options.ifname));
2475
2476 if (this.options.kind != 'wifi')
2477 return;
2478
2479 var networks = L.toArray(this.get('network'));
2480 if ($.inArray(iface.name(), networks) > -1)
2481 this.set('network', L.filterArray(networks, iface.name()));
2482 },
2483
2484 attachToInterface: function(iface)
2485 {
2486 if (!(iface instanceof L.NetworkModel.Interface))
2487 iface = L.NetworkModel.getInterface(iface);
2488
2489 if (!iface)
2490 return;
2491
2492 if (this.options.kind != 'wifi')
2493 {
2494 var ifnames = L.toArray(iface.get('ifname'));
2495 if ($.inArray(this.options.ifname, ifnames) < 0)
2496 {
2497 ifnames.push(this.options.ifname);
2498 iface.set('ifname', (ifnames.length > 1) ? ifnames : ifnames[0]);
2499 }
2500 }
2501 else
2502 {
2503 var networks = L.toArray(this.get('network'));
2504 if ($.inArray(iface.name(), networks) < 0)
2505 {
2506 networks.push(iface.name());
2507 this.set('network', (networks.length > 1) ? networks : networks[0]);
2508 }
2509 }
2510 }
2511 });
2512
2513 this.NetworkModel.Interface = Class.extend({
2514 _status: function(key)
2515 {
2516 var s = L.NetworkModel._cache.ifstate;
2517
2518 for (var i = 0; i < s.length; i++)
2519 if (s[i]['interface'] == this.options.name)
2520 return key ? s[i][key] : s[i];
2521
2522 return undefined;
2523 },
2524
2525 get: function(key)
2526 {
2527 return L.NetworkModel._get('network', this.options.name, key);
2528 },
2529
2530 set: function(key, val)
2531 {
2532 return L.NetworkModel._set('network', this.options.name, key, val);
2533 },
2534
2535 name: function()
2536 {
2537 return this.options.name;
2538 },
2539
2540 protocol: function()
2541 {
2542 return (this.get('proto') || 'none');
2543 },
2544
2545 isUp: function()
2546 {
2547 return (this._status('up') === true);
2548 },
2549
2550 isVirtual: function()
2551 {
2552 return (typeof(this.options.sid) != 'string');
2553 },
2554
2555 getProtocol: function()
2556 {
2557 var prname = this.get('proto') || 'none';
2558 return L.NetworkModel._protos[prname] || L.NetworkModel._protos.none;
2559 },
2560
2561 getUptime: function()
2562 {
2563 var uptime = this._status('uptime');
2564 return isNaN(uptime) ? 0 : uptime;
2565 },
2566
2567 getDevice: function(resolveAlias)
2568 {
2569 if (this.options.l3dev)
2570 return L.NetworkModel.getDevice(this.options.l3dev);
2571
2572 return undefined;
2573 },
2574
2575 getPhysdev: function()
2576 {
2577 if (this.options.l2dev)
2578 return L.NetworkModel.getDevice(this.options.l2dev);
2579
2580 return undefined;
2581 },
2582
2583 getSubdevices: function()
2584 {
2585 var rv = [ ];
2586 var dev = this.options.l2dev ?
2587 L.NetworkModel._devs[this.options.l2dev] : undefined;
2588
2589 if (dev && dev.kind == 'bridge' && dev.ports && dev.ports.length)
2590 for (var i = 0; i < dev.ports.length; i++)
2591 rv.push(L.NetworkModel.getDevice(dev.ports[i]));
2592
2593 return rv;
2594 },
2595
2596 getIPv4Addrs: function(mask)
2597 {
2598 var rv = [ ];
2599 var addrs = this._status('ipv4-address');
2600
2601 if (addrs)
2602 for (var i = 0; i < addrs.length; i++)
2603 if (!mask)
2604 rv.push(addrs[i].address);
2605 else
2606 rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
2607
2608 return rv;
2609 },
2610
2611 getIPv6Addrs: function(mask)
2612 {
2613 var rv = [ ];
2614 var addrs;
2615
2616 addrs = this._status('ipv6-address');
2617
2618 if (addrs)
2619 for (var i = 0; i < addrs.length; i++)
2620 if (!mask)
2621 rv.push(addrs[i].address);
2622 else
2623 rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
2624
2625 addrs = this._status('ipv6-prefix-assignment');
2626
2627 if (addrs)
2628 for (var i = 0; i < addrs.length; i++)
2629 if (!mask)
2630 rv.push('%s1'.format(addrs[i].address));
2631 else
2632 rv.push('%s1/%d'.format(addrs[i].address, addrs[i].mask));
2633
2634 return rv;
2635 },
2636
2637 getDNSAddrs: function()
2638 {
2639 var rv = [ ];
2640 var addrs = this._status('dns-server');
2641
2642 if (addrs)
2643 for (var i = 0; i < addrs.length; i++)
2644 rv.push(addrs[i]);
2645
2646 return rv;
2647 },
2648
2649 getIPv4DNS: function()
2650 {
2651 var rv = [ ];
2652 var dns = this._status('dns-server');
2653
2654 if (dns)
2655 for (var i = 0; i < dns.length; i++)
2656 if (dns[i].indexOf(':') == -1)
2657 rv.push(dns[i]);
2658
2659 return rv;
2660 },
2661
2662 getIPv6DNS: function()
2663 {
2664 var rv = [ ];
2665 var dns = this._status('dns-server');
2666
2667 if (dns)
2668 for (var i = 0; i < dns.length; i++)
2669 if (dns[i].indexOf(':') > -1)
2670 rv.push(dns[i]);
2671
2672 return rv;
2673 },
2674
2675 getIPv4Gateway: function()
2676 {
2677 var rt = this._status('route');
2678
2679 if (rt)
2680 for (var i = 0; i < rt.length; i++)
2681 if (rt[i].target == '0.0.0.0' && rt[i].mask == 0)
2682 return rt[i].nexthop;
2683
2684 return undefined;
2685 },
2686
2687 getIPv6Gateway: function()
2688 {
2689 var rt = this._status('route');
2690
2691 if (rt)
2692 for (var i = 0; i < rt.length; i++)
2693 if (rt[i].target == '::' && rt[i].mask == 0)
2694 return rt[i].nexthop;
2695
2696 return undefined;
2697 },
2698
2699 getStatistics: function()
2700 {
2701 var dev = this.getDevice() || new L.NetworkModel.Device({});
2702 return dev.getStatistics();
2703 },
2704
2705 getTrafficHistory: function()
2706 {
2707 var dev = this.getDevice() || new L.NetworkModel.Device({});
2708 return dev.getTrafficHistory();
2709 },
2710
2711 setDevices: function(devs)
2712 {
2713 var dev = this.getPhysdev();
2714 var old_devs = [ ];
2715 var changed = false;
2716
2717 if (dev && dev.isBridge())
2718 old_devs = this.getSubdevices();
2719 else if (dev)
2720 old_devs = [ dev ];
2721
2722 if (old_devs.length != devs.length)
2723 changed = true;
2724 else
2725 for (var i = 0; i < old_devs.length; i++)
2726 {
2727 var dev = devs[i];
2728
2729 if (dev instanceof L.NetworkModel.Device)
2730 dev = dev.name();
2731
2732 if (!dev || old_devs[i].name() != dev)
2733 {
2734 changed = true;
2735 break;
2736 }
2737 }
2738
2739 if (changed)
2740 {
2741 for (var i = 0; i < old_devs.length; i++)
2742 old_devs[i].removeFromInterface(this);
2743
2744 for (var i = 0; i < devs.length; i++)
2745 {
2746 var dev = devs[i];
2747
2748 if (!(dev instanceof L.NetworkModel.Device))
2749 dev = L.NetworkModel.getDevice(dev);
2750
2751 if (dev)
2752 dev.attachToInterface(this);
2753 }
2754 }
2755 },
2756
2757 changeProtocol: function(proto)
2758 {
2759 var pr = L.NetworkModel._protos[proto];
2760
2761 if (!pr)
2762 return;
2763
2764 for (var opt in (this.get() || { }))
2765 {
2766 switch (opt)
2767 {
2768 case 'type':
2769 case 'ifname':
2770 case 'macaddr':
2771 if (pr.virtual)
2772 this.set(opt, undefined);
2773 break;
2774
2775 case 'auto':
2776 case 'mtu':
2777 break;
2778
2779 case 'proto':
2780 this.set(opt, pr.protocol);
2781 break;
2782
2783 default:
2784 this.set(opt, undefined);
2785 break;
2786 }
2787 }
2788 },
2789
2790 createForm: function(mapwidget)
2791 {
2792 var self = this;
2793 var proto = self.getProtocol();
2794 var device = self.getDevice();
2795
2796 if (!mapwidget)
2797 mapwidget = L.cbi.Map;
2798
2799 var map = new mapwidget('network', {
2800 caption: L.tr('Configure "%s"').format(self.name())
2801 });
2802
2803 var section = map.section(L.cbi.SingleSection, self.name(), {
2804 anonymous: true
2805 });
2806
2807 section.tab({
2808 id: 'general',
2809 caption: L.tr('General Settings')
2810 });
2811
2812 section.tab({
2813 id: 'advanced',
2814 caption: L.tr('Advanced Settings')
2815 });
2816
2817 section.tab({
2818 id: 'ipv6',
2819 caption: L.tr('IPv6')
2820 });
2821
2822 section.tab({
2823 id: 'physical',
2824 caption: L.tr('Physical Settings')
2825 });
2826
2827
2828 section.taboption('general', L.cbi.CheckboxValue, 'auto', {
2829 caption: L.tr('Start on boot'),
2830 optional: true,
2831 initial: true
2832 });
2833
2834 var pr = section.taboption('general', L.cbi.ListValue, 'proto', {
2835 caption: L.tr('Protocol')
2836 });
2837
2838 pr.ucivalue = function(sid) {
2839 return self.get('proto') || 'none';
2840 };
2841
2842 var ok = section.taboption('general', L.cbi.ButtonValue, '_confirm', {
2843 caption: L.tr('Really switch?'),
2844 description: L.tr('Changing the protocol will clear all configuration for this interface!'),
2845 text: L.tr('Change protocol')
2846 });
2847
2848 ok.on('click', function(ev) {
2849 self.changeProtocol(pr.formvalue(ev.data.sid));
2850 self.createForm(mapwidget).show();
2851 });
2852
2853 var protos = L.NetworkModel.getProtocols();
2854
2855 for (var i = 0; i < protos.length; i++)
2856 pr.value(protos[i].name, protos[i].description);
2857
2858 proto.populateForm(section, self);
2859
2860 if (!proto.virtual)
2861 {
2862 var br = section.taboption('physical', L.cbi.CheckboxValue, 'type', {
2863 caption: L.tr('Network bridge'),
2864 description: L.tr('Merges multiple devices into one logical bridge'),
2865 optional: true,
2866 enabled: 'bridge',
2867 disabled: '',
2868 initial: ''
2869 });
2870
2871 section.taboption('physical', L.cbi.DeviceList, '__iface_multi', {
2872 caption: L.tr('Devices'),
2873 multiple: true,
2874 bridges: false
2875 }).depends('type', true);
2876
2877 section.taboption('physical', L.cbi.DeviceList, '__iface_single', {
2878 caption: L.tr('Device'),
2879 multiple: false,
2880 bridges: true
2881 }).depends('type', false);
2882
2883 var mac = section.taboption('physical', L.cbi.InputValue, 'macaddr', {
2884 caption: L.tr('Override MAC'),
2885 optional: true,
2886 placeholder: device ? device.getMACAddress() : undefined,
2887 datatype: 'macaddr'
2888 })
2889
2890 mac.ucivalue = function(sid)
2891 {
2892 if (device)
2893 return device.get('macaddr');
2894
2895 return this.callSuper('ucivalue', sid);
2896 };
2897
2898 mac.save = function(sid)
2899 {
2900 if (!this.changed(sid))
2901 return false;
2902
2903 if (device)
2904 device.set('macaddr', this.formvalue(sid));
2905 else
2906 this.callSuper('set', sid);
2907
2908 return true;
2909 };
2910 }
2911
2912 section.taboption('physical', L.cbi.InputValue, 'mtu', {
2913 caption: L.tr('Override MTU'),
2914 optional: true,
2915 placeholder: device ? device.getMTU() : undefined,
2916 datatype: 'range(1, 9000)'
2917 });
2918
2919 section.taboption('physical', L.cbi.InputValue, 'metric', {
2920 caption: L.tr('Override Metric'),
2921 optional: true,
2922 placeholder: 0,
2923 datatype: 'uinteger'
2924 });
2925
2926 for (var field in section.fields)
2927 {
2928 switch (field)
2929 {
2930 case 'proto':
2931 break;
2932
2933 case '_confirm':
2934 for (var i = 0; i < protos.length; i++)
2935 if (protos[i].name != (this.get('proto') || 'none'))
2936 section.fields[field].depends('proto', protos[i].name);
2937 break;
2938
2939 default:
2940 section.fields[field].depends('proto', this.get('proto') || 'none', true);
2941 break;
2942 }
2943 }
2944
2945 return map;
2946 }
2947 });
2948
2949 this.NetworkModel.Protocol = this.NetworkModel.Interface.extend({
2950 description: '__unknown__',
2951 tunnel: false,
2952 virtual: false,
2953
2954 populateForm: function(section, iface)
2955 {
2956
2957 }
2958 });
2959
2960 this.system = {
2961 getSystemInfo: L.rpc.declare({
2962 object: 'system',
2963 method: 'info',
2964 expect: { '': { } }
2965 }),
2966
2967 getBoardInfo: L.rpc.declare({
2968 object: 'system',
2969 method: 'board',
2970 expect: { '': { } }
2971 }),
2972
2973 getDiskInfo: L.rpc.declare({
2974 object: 'luci2.system',
2975 method: 'diskfree',
2976 expect: { '': { } }
2977 }),
2978
2979 getInfo: function(cb)
2980 {
2981 L.rpc.batch();
2982
2983 this.getSystemInfo();
2984 this.getBoardInfo();
2985 this.getDiskInfo();
2986
2987 return L.rpc.flush().then(function(info) {
2988 var rv = { };
2989
2990 $.extend(rv, info[0]);
2991 $.extend(rv, info[1]);
2992 $.extend(rv, info[2]);
2993
2994 return rv;
2995 });
2996 },
2997
2998
2999 initList: L.rpc.declare({
3000 object: 'luci2.system',
3001 method: 'init_list',
3002 expect: { initscripts: [ ] },
3003 filter: function(data) {
3004 data.sort(function(a, b) { return (a.start || 0) - (b.start || 0) });
3005 return data;
3006 }
3007 }),
3008
3009 initEnabled: function(init, cb)
3010 {
3011 return this.initList().then(function(list) {
3012 for (var i = 0; i < list.length; i++)
3013 if (list[i].name == init)
3014 return !!list[i].enabled;
3015
3016 return false;
3017 });
3018 },
3019
3020 initRun: L.rpc.declare({
3021 object: 'luci2.system',
3022 method: 'init_action',
3023 params: [ 'name', 'action' ],
3024 filter: function(data) {
3025 return (data == 0);
3026 }
3027 }),
3028
3029 initStart: function(init, cb) { return L.system.initRun(init, 'start', cb) },
3030 initStop: function(init, cb) { return L.system.initRun(init, 'stop', cb) },
3031 initRestart: function(init, cb) { return L.system.initRun(init, 'restart', cb) },
3032 initReload: function(init, cb) { return L.system.initRun(init, 'reload', cb) },
3033 initEnable: function(init, cb) { return L.system.initRun(init, 'enable', cb) },
3034 initDisable: function(init, cb) { return L.system.initRun(init, 'disable', cb) },
3035
3036
3037 performReboot: L.rpc.declare({
3038 object: 'luci2.system',
3039 method: 'reboot'
3040 })
3041 };
3042
3043 this.session = {
3044
3045 login: L.rpc.declare({
3046 object: 'session',
3047 method: 'login',
3048 params: [ 'username', 'password' ],
3049 expect: { '': { } }
3050 }),
3051
3052 access: L.rpc.declare({
3053 object: 'session',
3054 method: 'access',
3055 params: [ 'scope', 'object', 'function' ],
3056 expect: { access: false }
3057 }),
3058
3059 isAlive: function()
3060 {
3061 return L.session.access('ubus', 'session', 'access');
3062 },
3063
3064 startHeartbeat: function()
3065 {
3066 this._hearbeatInterval = window.setInterval(function() {
3067 L.session.isAlive().then(function(alive) {
3068 if (!alive)
3069 {
3070 L.session.stopHeartbeat();
3071 L.ui.login(true);
3072 }
3073
3074 });
3075 }, L.globals.timeout * 2);
3076 },
3077
3078 stopHeartbeat: function()
3079 {
3080 if (typeof(this._hearbeatInterval) != 'undefined')
3081 {
3082 window.clearInterval(this._hearbeatInterval);
3083 delete this._hearbeatInterval;
3084 }
3085 },
3086
3087
3088 _acls: { },
3089
3090 _fetch_acls: L.rpc.declare({
3091 object: 'session',
3092 method: 'access',
3093 expect: { '': { } }
3094 }),
3095
3096 _fetch_acls_cb: function(acls)
3097 {
3098 L.session._acls = acls;
3099 },
3100
3101 updateACLs: function()
3102 {
3103 return L.session._fetch_acls()
3104 .then(L.session._fetch_acls_cb);
3105 },
3106
3107 hasACL: function(scope, object, func)
3108 {
3109 var acls = L.session._acls;
3110
3111 if (typeof(func) == 'undefined')
3112 return (acls && acls[scope] && acls[scope][object]);
3113
3114 if (acls && acls[scope] && acls[scope][object])
3115 for (var i = 0; i < acls[scope][object].length; i++)
3116 if (acls[scope][object][i] == func)
3117 return true;
3118
3119 return false;
3120 }
3121 };
3122
3123 this.ui = {
3124
3125 saveScrollTop: function()
3126 {
3127 this._scroll_top = $(document).scrollTop();
3128 },
3129
3130 restoreScrollTop: function()
3131 {
3132 if (typeof(this._scroll_top) == 'undefined')
3133 return;
3134
3135 $(document).scrollTop(this._scroll_top);
3136
3137 delete this._scroll_top;
3138 },
3139
3140 loading: function(enable)
3141 {
3142 var win = $(window);
3143 var body = $('body');
3144
3145 var state = L.ui._loading || (L.ui._loading = {
3146 modal: $('<div />')
3147 .css('z-index', 2000)
3148 .addClass('modal fade')
3149 .append($('<div />')
3150 .addClass('modal-dialog')
3151 .append($('<div />')
3152 .addClass('modal-content luci2-modal-loader')
3153 .append($('<div />')
3154 .addClass('modal-body')
3155 .text(L.tr('Loading data…')))))
3156 .appendTo(body)
3157 .modal({
3158 backdrop: 'static',
3159 keyboard: false
3160 })
3161 });
3162
3163 state.modal.modal(enable ? 'show' : 'hide');
3164 },
3165
3166 dialog: function(title, content, options)
3167 {
3168 var win = $(window);
3169 var body = $('body');
3170
3171 var state = L.ui._dialog || (L.ui._dialog = {
3172 dialog: $('<div />')
3173 .addClass('modal fade')
3174 .append($('<div />')
3175 .addClass('modal-dialog')
3176 .append($('<div />')
3177 .addClass('modal-content')
3178 .append($('<div />')
3179 .addClass('modal-header')
3180 .append('<h4 />')
3181 .addClass('modal-title'))
3182 .append($('<div />')
3183 .addClass('modal-body'))
3184 .append($('<div />')
3185 .addClass('modal-footer')
3186 .append(L.ui.button(L.tr('Close'), 'primary')
3187 .click(function() {
3188 $(this).parents('div.modal').modal('hide');
3189 })))))
3190 .appendTo(body)
3191 });
3192
3193 if (typeof(options) != 'object')
3194 options = { };
3195
3196 if (title === false)
3197 {
3198 state.dialog.modal('hide');
3199
3200 return state.dialog;
3201 }
3202
3203 var cnt = state.dialog.children().children().children('div.modal-body');
3204 var ftr = state.dialog.children().children().children('div.modal-footer');
3205
3206 ftr.empty().show();
3207
3208 if (options.style == 'confirm')
3209 {
3210 ftr.append(L.ui.button(L.tr('Ok'), 'primary')
3211 .click(options.confirm || function() { L.ui.dialog(false) }));
3212
3213 ftr.append(L.ui.button(L.tr('Cancel'), 'default')
3214 .click(options.cancel || function() { L.ui.dialog(false) }));
3215 }
3216 else if (options.style == 'close')
3217 {
3218 ftr.append(L.ui.button(L.tr('Close'), 'primary')
3219 .click(options.close || function() { L.ui.dialog(false) }));
3220 }
3221 else if (options.style == 'wait')
3222 {
3223 ftr.append(L.ui.button(L.tr('Close'), 'primary')
3224 .attr('disabled', true));
3225 }
3226
3227 if (options.wide)
3228 {
3229 state.dialog.addClass('wide');
3230 }
3231 else
3232 {
3233 state.dialog.removeClass('wide');
3234 }
3235
3236 state.dialog.find('h4:first').text(title);
3237 state.dialog.modal('show');
3238
3239 cnt.empty().append(content);
3240
3241 return state.dialog;
3242 },
3243
3244 upload: function(title, content, options)
3245 {
3246 var state = L.ui._upload || (L.ui._upload = {
3247 form: $('<form />')
3248 .attr('method', 'post')
3249 .attr('action', '/cgi-bin/luci-upload')
3250 .attr('enctype', 'multipart/form-data')
3251 .attr('target', 'cbi-fileupload-frame')
3252 .append($('<p />'))
3253 .append($('<input />')
3254 .attr('type', 'hidden')
3255 .attr('name', 'sessionid'))
3256 .append($('<input />')
3257 .attr('type', 'hidden')
3258 .attr('name', 'filename'))
3259 .append($('<input />')
3260 .attr('type', 'file')
3261 .attr('name', 'filedata')
3262 .addClass('cbi-input-file'))
3263 .append($('<div />')
3264 .css('width', '100%')
3265 .addClass('progress progress-striped active')
3266 .append($('<div />')
3267 .addClass('progress-bar')
3268 .css('width', '100%')))
3269 .append($('<iframe />')
3270 .addClass('pull-right')
3271 .attr('name', 'cbi-fileupload-frame')
3272 .css('width', '1px')
3273 .css('height', '1px')
3274 .css('visibility', 'hidden')),
3275
3276 finish_cb: function(ev) {
3277 $(this).off('load');
3278
3279 var body = (this.contentDocument || this.contentWindow.document).body;
3280 if (body.firstChild.tagName.toLowerCase() == 'pre')
3281 body = body.firstChild;
3282
3283 var json;
3284 try {
3285 json = $.parseJSON(body.innerHTML);
3286 } catch(e) {
3287 json = {
3288 message: L.tr('Invalid server response received'),
3289 error: [ -1, L.tr('Invalid data') ]
3290 };
3291 };
3292
3293 if (json.error)
3294 {
3295 L.ui.dialog(L.tr('File upload'), [
3296 $('<p />').text(L.tr('The file upload failed with the server response below:')),
3297 $('<pre />').addClass('alert-message').text(json.message || json.error[1]),
3298 $('<p />').text(L.tr('In case of network problems try uploading the file again.'))
3299 ], { style: 'close' });
3300 }
3301 else if (typeof(state.success_cb) == 'function')
3302 {
3303 state.success_cb(json);
3304 }
3305 },
3306
3307 confirm_cb: function() {
3308 var f = state.form.find('.cbi-input-file');
3309 var b = state.form.find('.progress');
3310 var p = state.form.find('p');
3311
3312 if (!f.val())
3313 return;
3314
3315 state.form.find('iframe').on('load', state.finish_cb);
3316 state.form.submit();
3317
3318 f.hide();
3319 b.show();
3320 p.text(L.tr('File upload in progress …'));
3321
3322 state.form.parent().parent().find('button').prop('disabled', true);
3323 }
3324 });
3325
3326 state.form.find('.progress').hide();
3327 state.form.find('.cbi-input-file').val('').show();
3328 state.form.find('p').text(content || L.tr('Select the file to upload and press "%s" to proceed.').format(L.tr('Ok')));
3329
3330 state.form.find('[name=sessionid]').val(L.globals.sid);
3331 state.form.find('[name=filename]').val(options.filename);
3332
3333 state.success_cb = options.success;
3334
3335 L.ui.dialog(title || L.tr('File upload'), state.form, {
3336 style: 'confirm',
3337 confirm: state.confirm_cb
3338 });
3339 },
3340
3341 reconnect: function()
3342 {
3343 var protocols = (location.protocol == 'https:') ? [ 'http', 'https' ] : [ 'http' ];
3344 var ports = (location.protocol == 'https:') ? [ 80, location.port || 443 ] : [ location.port || 80 ];
3345 var address = location.hostname.match(/^[A-Fa-f0-9]*:[A-Fa-f0-9:]+$/) ? '[' + location.hostname + ']' : location.hostname;
3346 var images = $();
3347 var interval, timeout;
3348
3349 L.ui.dialog(
3350 L.tr('Waiting for device'), [
3351 $('<p />').text(L.tr('Please stand by while the device is reconfiguring …')),
3352 $('<div />')
3353 .css('width', '100%')
3354 .addClass('progressbar')
3355 .addClass('intermediate')
3356 .append($('<div />')
3357 .css('width', '100%'))
3358 ], { style: 'wait' }
3359 );
3360
3361 for (var i = 0; i < protocols.length; i++)
3362 images = images.add($('<img />').attr('url', protocols[i] + '://' + address + ':' + ports[i]));
3363
3364 //L.network.getNetworkStatus(function(s) {
3365 // for (var i = 0; i < protocols.length; i++)
3366 // {
3367 // for (var j = 0; j < s.length; j++)
3368 // {
3369 // for (var k = 0; k < s[j]['ipv4-address'].length; k++)
3370 // images = images.add($('<img />').attr('url', protocols[i] + '://' + s[j]['ipv4-address'][k].address + ':' + ports[i]));
3371 //
3372 // for (var l = 0; l < s[j]['ipv6-address'].length; l++)
3373 // images = images.add($('<img />').attr('url', protocols[i] + '://[' + s[j]['ipv6-address'][l].address + ']:' + ports[i]));
3374 // }
3375 // }
3376 //}).then(function() {
3377 images.on('load', function() {
3378 var url = this.getAttribute('url');
3379 L.session.isAlive().then(function(access) {
3380 if (access)
3381 {
3382 window.clearTimeout(timeout);
3383 window.clearInterval(interval);
3384 L.ui.dialog(false);
3385 images = null;
3386 }
3387 else
3388 {
3389 location.href = url;
3390 }
3391 });
3392 });
3393
3394 interval = window.setInterval(function() {
3395 images.each(function() {
3396 this.setAttribute('src', this.getAttribute('url') + L.globals.resource + '/icons/loading.gif?r=' + Math.random());
3397 });
3398 }, 5000);
3399
3400 timeout = window.setTimeout(function() {
3401 window.clearInterval(interval);
3402 images.off('load');
3403
3404 L.ui.dialog(
3405 L.tr('Device not responding'),
3406 L.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'),
3407 { style: 'close' }
3408 );
3409 }, 180000);
3410 //});
3411 },
3412
3413 login: function(invalid)
3414 {
3415 var state = L.ui._login || (L.ui._login = {
3416 form: $('<form />')
3417 .attr('target', '')
3418 .attr('method', 'post')
3419 .append($('<p />')
3420 .addClass('alert-message')
3421 .text(L.tr('Wrong username or password given!')))
3422 .append($('<p />')
3423 .append($('<label />')
3424 .text(L.tr('Username'))
3425 .append($('<br />'))
3426 .append($('<input />')
3427 .attr('type', 'text')
3428 .attr('name', 'username')
3429 .attr('value', 'root')
3430 .addClass('form-control')
3431 .keypress(function(ev) {
3432 if (ev.which == 10 || ev.which == 13)
3433 state.confirm_cb();
3434 }))))
3435 .append($('<p />')
3436 .append($('<label />')
3437 .text(L.tr('Password'))
3438 .append($('<br />'))
3439 .append($('<input />')
3440 .attr('type', 'password')
3441 .attr('name', 'password')
3442 .addClass('form-control')
3443 .keypress(function(ev) {
3444 if (ev.which == 10 || ev.which == 13)
3445 state.confirm_cb();
3446 }))))
3447 .append($('<p />')
3448 .text(L.tr('Enter your username and password above, then click "%s" to proceed.').format(L.tr('Ok')))),
3449
3450 response_cb: function(response) {
3451 if (!response.ubus_rpc_session)
3452 {
3453 L.ui.login(true);
3454 }
3455 else
3456 {
3457 L.globals.sid = response.ubus_rpc_session;
3458 L.setHash('id', L.globals.sid);
3459 L.session.startHeartbeat();
3460 L.ui.dialog(false);
3461 state.deferred.resolve();
3462 }
3463 },
3464
3465 confirm_cb: function() {
3466 var u = state.form.find('[name=username]').val();
3467 var p = state.form.find('[name=password]').val();
3468
3469 if (!u)
3470 return;
3471
3472 L.ui.dialog(
3473 L.tr('Logging in'), [
3474 $('<p />').text(L.tr('Log in in progress …')),
3475 $('<div />')
3476 .css('width', '100%')
3477 .addClass('progressbar')
3478 .addClass('intermediate')
3479 .append($('<div />')
3480 .css('width', '100%'))
3481 ], { style: 'wait' }
3482 );
3483
3484 L.globals.sid = '00000000000000000000000000000000';
3485 L.session.login(u, p).then(state.response_cb);
3486 }
3487 });
3488
3489 if (!state.deferred || state.deferred.state() != 'pending')
3490 state.deferred = $.Deferred();
3491
3492 /* try to find sid from hash */
3493 var sid = L.getHash('id');
3494 if (sid && sid.match(/^[a-f0-9]{32}$/))
3495 {
3496 L.globals.sid = sid;
3497 L.session.isAlive().then(function(access) {
3498 if (access)
3499 {
3500 L.session.startHeartbeat();
3501 state.deferred.resolve();
3502 }
3503 else
3504 {
3505 L.setHash('id', undefined);
3506 L.ui.login();
3507 }
3508 });
3509
3510 return state.deferred;
3511 }
3512
3513 if (invalid)
3514 state.form.find('.alert-message').show();
3515 else
3516 state.form.find('.alert-message').hide();
3517
3518 L.ui.dialog(L.tr('Authorization Required'), state.form, {
3519 style: 'confirm',
3520 confirm: state.confirm_cb
3521 });
3522
3523 state.form.find('[name=password]').focus();
3524
3525 return state.deferred;
3526 },
3527
3528 cryptPassword: L.rpc.declare({
3529 object: 'luci2.ui',
3530 method: 'crypt',
3531 params: [ 'data' ],
3532 expect: { crypt: '' }
3533 }),
3534
3535
3536 _acl_merge_scope: function(acl_scope, scope)
3537 {
3538 if ($.isArray(scope))
3539 {
3540 for (var i = 0; i < scope.length; i++)
3541 acl_scope[scope[i]] = true;
3542 }
3543 else if ($.isPlainObject(scope))
3544 {
3545 for (var object_name in scope)
3546 {
3547 if (!$.isArray(scope[object_name]))
3548 continue;
3549
3550 var acl_object = acl_scope[object_name] || (acl_scope[object_name] = { });
3551
3552 for (var i = 0; i < scope[object_name].length; i++)
3553 acl_object[scope[object_name][i]] = true;
3554 }
3555 }
3556 },
3557
3558 _acl_merge_permission: function(acl_perm, perm)
3559 {
3560 if ($.isPlainObject(perm))
3561 {
3562 for (var scope_name in perm)
3563 {
3564 var acl_scope = acl_perm[scope_name] || (acl_perm[scope_name] = { });
3565 this._acl_merge_scope(acl_scope, perm[scope_name]);
3566 }
3567 }
3568 },
3569
3570 _acl_merge_group: function(acl_group, group)
3571 {
3572 if ($.isPlainObject(group))
3573 {
3574 if (!acl_group.description)
3575 acl_group.description = group.description;
3576
3577 if (group.read)
3578 {
3579 var acl_perm = acl_group.read || (acl_group.read = { });
3580 this._acl_merge_permission(acl_perm, group.read);
3581 }
3582
3583 if (group.write)
3584 {
3585 var acl_perm = acl_group.write || (acl_group.write = { });
3586 this._acl_merge_permission(acl_perm, group.write);
3587 }
3588 }
3589 },
3590
3591 _acl_merge_tree: function(acl_tree, tree)
3592 {
3593 if ($.isPlainObject(tree))
3594 {
3595 for (var group_name in tree)
3596 {
3597 var acl_group = acl_tree[group_name] || (acl_tree[group_name] = { });
3598 this._acl_merge_group(acl_group, tree[group_name]);
3599 }
3600 }
3601 },
3602
3603 listAvailableACLs: L.rpc.declare({
3604 object: 'luci2.ui',
3605 method: 'acls',
3606 expect: { acls: [ ] },
3607 filter: function(trees) {
3608 var acl_tree = { };
3609 for (var i = 0; i < trees.length; i++)
3610 L.ui._acl_merge_tree(acl_tree, trees[i]);
3611 return acl_tree;
3612 }
3613 }),
3614
3615 _render_change_indicator: function()
3616 {
3617 return $('<ul />')
3618 .addClass('nav navbar-nav navbar-right')
3619 .append($('<li />')
3620 .append($('<a />')
3621 .attr('id', 'changes')
3622 .attr('href', '#')
3623 .append($('<span />')
3624 .addClass('label label-info'))));
3625 },
3626
3627 renderMainMenu: L.rpc.declare({
3628 object: 'luci2.ui',
3629 method: 'menu',
3630 expect: { menu: { } },
3631 filter: function(entries) {
3632 L.globals.mainMenu = new L.ui.menu();
3633 L.globals.mainMenu.entries(entries);
3634
3635 $('#mainmenu')
3636 .empty()
3637 .append(L.globals.mainMenu.render(0, 1))
3638 .append(L.ui._render_change_indicator());
3639 }
3640 }),
3641
3642 renderViewMenu: function()
3643 {
3644 $('#viewmenu')
3645 .empty()
3646 .append(L.globals.mainMenu.render(2, 900));
3647 },
3648
3649 renderView: function()
3650 {
3651 var node = arguments[0];
3652 var name = node.view.split(/\//).join('.');
3653 var cname = L.toClassName(name);
3654 var views = L.views || (L.views = { });
3655 var args = [ ];
3656
3657 for (var i = 1; i < arguments.length; i++)
3658 args.push(arguments[i]);
3659
3660 if (L.globals.currentView)
3661 L.globals.currentView.finish();
3662
3663 L.ui.renderViewMenu();
3664 L.setHash('view', node.view);
3665
3666 if (views[cname] instanceof L.ui.view)
3667 {
3668 L.globals.currentView = views[cname];
3669 return views[cname].render.apply(views[cname], args);
3670 }
3671
3672 var url = L.globals.resource + '/view/' + name + '.js';
3673
3674 return $.ajax(url, {
3675 method: 'GET',
3676 cache: true,
3677 dataType: 'text'
3678 }).then(function(data) {
3679 try {
3680 var viewConstructorSource = (
3681 '(function(L, $) { ' +
3682 'return %s' +
3683 '})(L, $);\n\n' +
3684 '//@ sourceURL=%s'
3685 ).format(data, url);
3686
3687 var viewConstructor = eval(viewConstructorSource);
3688
3689 views[cname] = new viewConstructor({
3690 name: name,
3691 acls: node.write || { }
3692 });
3693
3694 L.globals.currentView = views[cname];
3695 return views[cname].render.apply(views[cname], args);
3696 }
3697 catch(e) {
3698 alert('Unable to instantiate view "%s": %s'.format(url, e));
3699 };
3700
3701 return $.Deferred().resolve();
3702 });
3703 },
3704
3705 changeView: function()
3706 {
3707 var name = L.getHash('view');
3708 var node = L.globals.defaultNode;
3709
3710 if (name && L.globals.mainMenu)
3711 node = L.globals.mainMenu.getNode(name);
3712
3713 if (node)
3714 {
3715 L.ui.loading(true);
3716 L.ui.renderView(node).then(function() {
3717 L.ui.loading(false);
3718 });
3719 }
3720 },
3721
3722 updateHostname: function()
3723 {
3724 return L.system.getBoardInfo().then(function(info) {
3725 if (info.hostname)
3726 $('#hostname').text(info.hostname);
3727 });
3728 },
3729
3730 updateChanges: function()
3731 {
3732 return L.uci.changes().then(function(changes) {
3733 var n = 0;
3734 var html = '';
3735
3736 for (var config in changes)
3737 {
3738 var log = [ ];
3739
3740 for (var i = 0; i < changes[config].length; i++)
3741 {
3742 var c = changes[config][i];
3743
3744 switch (c[0])
3745 {
3746 case 'order':
3747 log.push('uci reorder %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
3748 break;
3749
3750 case 'remove':
3751 if (c.length < 3)
3752 log.push('uci delete %s.<del>%s</del>'.format(config, c[1]));
3753 else
3754 log.push('uci delete %s.%s.<del>%s</del>'.format(config, c[1], c[2]));
3755 break;
3756
3757 case 'rename':
3758 if (c.length < 4)
3759 log.push('uci rename %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3]));
3760 else
3761 log.push('uci rename %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
3762 break;
3763
3764 case 'add':
3765 log.push('uci add %s <ins>%s</ins> (= <ins><strong>%s</strong></ins>)'.format(config, c[2], c[1]));
3766 break;
3767
3768 case 'list-add':
3769 log.push('uci add_list %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
3770 break;
3771
3772 case 'list-del':
3773 log.push('uci del_list %s.%s.<del>%s=<strong>%s</strong></del>'.format(config, c[1], c[2], c[3], c[4]));
3774 break;
3775
3776 case 'set':
3777 if (c.length < 4)
3778 log.push('uci set %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
3779 else
3780 log.push('uci set %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
3781 break;
3782 }
3783 }
3784
3785 html += '<code>/etc/config/%s</code><pre class="uci-changes">%s</pre>'.format(config, log.join('\n'));
3786 n += changes[config].length;
3787 }
3788
3789 if (n > 0)
3790 $('#changes')
3791 .click(function(ev) {
3792 L.ui.dialog(L.tr('Staged configuration changes'), html, {
3793 style: 'confirm',
3794 confirm: function() {
3795 L.uci.apply().then(
3796 function(code) { alert('Success with code ' + code); },
3797 function(code) { alert('Error with code ' + code); }
3798 );
3799 }
3800 });
3801 ev.preventDefault();
3802 })
3803 .children('span')
3804 .show()
3805 .text(L.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n));
3806 else
3807 $('#changes').children('span').hide();
3808 });
3809 },
3810
3811 init: function()
3812 {
3813 L.ui.loading(true);
3814
3815 $.when(
3816 L.session.updateACLs(),
3817 L.ui.updateHostname(),
3818 L.ui.updateChanges(),
3819 L.ui.renderMainMenu(),
3820 L.NetworkModel.init()
3821 ).then(function() {
3822 L.ui.renderView(L.globals.defaultNode).then(function() {
3823 L.ui.loading(false);
3824 });
3825
3826 $(window).on('hashchange', function() {
3827 L.ui.changeView();
3828 });
3829 });
3830 },
3831
3832 button: function(label, style, title)
3833 {
3834 style = style || 'default';
3835
3836 return $('<button />')
3837 .attr('type', 'button')
3838 .attr('title', title ? title : '')
3839 .addClass('btn btn-' + style)
3840 .text(label);
3841 }
3842 };
3843
3844 this.ui.AbstractWidget = Class.extend({
3845 i18n: function(text) {
3846 return text;
3847 },
3848
3849 label: function() {
3850 var key = arguments[0];
3851 var args = [ ];
3852
3853 for (var i = 1; i < arguments.length; i++)
3854 args.push(arguments[i]);
3855
3856 switch (typeof(this.options[key]))
3857 {
3858 case 'undefined':
3859 return '';
3860
3861 case 'function':
3862 return this.options[key].apply(this, args);
3863
3864 default:
3865 return ''.format.apply('' + this.options[key], args);
3866 }
3867 },
3868
3869 toString: function() {
3870 return $('<div />').append(this.render()).html();
3871 },
3872
3873 insertInto: function(id) {
3874 return $(id).empty().append(this.render());
3875 },
3876
3877 appendTo: function(id) {
3878 return $(id).append(this.render());
3879 },
3880
3881 on: function(evname, evfunc)
3882 {
3883 var evnames = L.toArray(evname);
3884
3885 if (!this.events)
3886 this.events = { };
3887
3888 for (var i = 0; i < evnames.length; i++)
3889 this.events[evnames[i]] = evfunc;
3890
3891 return this;
3892 },
3893
3894 trigger: function(evname, evdata)
3895 {
3896 if (this.events)
3897 {
3898 var evnames = L.toArray(evname);
3899
3900 for (var i = 0; i < evnames.length; i++)
3901 if (this.events[evnames[i]])
3902 this.events[evnames[i]].call(this, evdata);
3903 }
3904
3905 return this;
3906 }
3907 });
3908
3909 this.ui.view = this.ui.AbstractWidget.extend({
3910 _fetch_template: function()
3911 {
3912 return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', {
3913 method: 'GET',
3914 cache: true,
3915 dataType: 'text',
3916 success: function(data) {
3917 data = data.replace(/<%([#:=])?(.+?)%>/g, function(match, p1, p2) {
3918 p2 = p2.replace(/^\s+/, '').replace(/\s+$/, '');
3919 switch (p1)
3920 {
3921 case '#':
3922 return '';
3923
3924 case ':':
3925 return L.tr(p2);
3926
3927 case '=':
3928 return L.globals[p2] || '';
3929
3930 default:
3931 return '(?' + match + ')';
3932 }
3933 });
3934
3935 $('#maincontent').append(data);
3936 }
3937 });
3938 },
3939
3940 execute: function()
3941 {
3942 throw "Not implemented";
3943 },
3944
3945 render: function()
3946 {
3947 var container = $('#maincontent');
3948
3949 container.empty();
3950
3951 if (this.title)
3952 container.append($('<h2 />').append(this.title));
3953
3954 if (this.description)
3955 container.append($('<p />').append(this.description));
3956
3957 var self = this;
3958 var args = [ ];
3959
3960 for (var i = 0; i < arguments.length; i++)
3961 args.push(arguments[i]);
3962
3963 return this._fetch_template().then(function() {
3964 return L.deferrable(self.execute.apply(self, args));
3965 });
3966 },
3967
3968 repeat: function(func, interval)
3969 {
3970 var self = this;
3971
3972 if (!self._timeouts)
3973 self._timeouts = [ ];
3974
3975 var index = self._timeouts.length;
3976
3977 if (typeof(interval) != 'number')
3978 interval = 5000;
3979
3980 var setTimer, runTimer;
3981
3982 setTimer = function() {
3983 if (self._timeouts)
3984 self._timeouts[index] = window.setTimeout(runTimer, interval);
3985 };
3986
3987 runTimer = function() {
3988 L.deferrable(func.call(self)).then(setTimer, setTimer);
3989 };
3990
3991 runTimer();
3992 },
3993
3994 finish: function()
3995 {
3996 if ($.isArray(this._timeouts))
3997 {
3998 for (var i = 0; i < this._timeouts.length; i++)
3999 window.clearTimeout(this._timeouts[i]);
4000
4001 delete this._timeouts;
4002 }
4003 }
4004 });
4005
4006 this.ui.menu = this.ui.AbstractWidget.extend({
4007 init: function() {
4008 this._nodes = { };
4009 },
4010
4011 entries: function(entries)
4012 {
4013 for (var entry in entries)
4014 {
4015 var path = entry.split(/\//);
4016 var node = this._nodes;
4017
4018 for (i = 0; i < path.length; i++)
4019 {
4020 if (!node.childs)
4021 node.childs = { };
4022
4023 if (!node.childs[path[i]])
4024 node.childs[path[i]] = { };
4025
4026 node = node.childs[path[i]];
4027 }
4028
4029 $.extend(node, entries[entry]);
4030 }
4031 },
4032
4033 _indexcmp: function(a, b)
4034 {
4035 var x = a.index || 0;
4036 var y = b.index || 0;
4037 return (x - y);
4038 },
4039
4040 firstChildView: function(node)
4041 {
4042 if (node.view)
4043 return node;
4044
4045 var nodes = [ ];
4046 for (var child in (node.childs || { }))
4047 nodes.push(node.childs[child]);
4048
4049 nodes.sort(this._indexcmp);
4050
4051 for (var i = 0; i < nodes.length; i++)
4052 {
4053 var child = this.firstChildView(nodes[i]);
4054 if (child)
4055 {
4056 for (var key in child)
4057 if (!node.hasOwnProperty(key) && child.hasOwnProperty(key))
4058 node[key] = child[key];
4059
4060 return node;
4061 }
4062 }
4063
4064 return undefined;
4065 },
4066
4067 _onclick: function(ev)
4068 {
4069 L.setHash('view', ev.data);
4070
4071 ev.preventDefault();
4072 this.blur();
4073 },
4074
4075 _render: function(childs, level, min, max)
4076 {
4077 var nodes = [ ];
4078 for (var node in childs)
4079 {
4080 var child = this.firstChildView(childs[node]);
4081 if (child)
4082 nodes.push(childs[node]);
4083 }
4084
4085 nodes.sort(this._indexcmp);
4086
4087 var list = $('<ul />');
4088
4089 if (level == 0)
4090 list.addClass('nav').addClass('navbar-nav');
4091 else if (level == 1)
4092 list.addClass('dropdown-menu').addClass('navbar-inverse');
4093
4094 for (var i = 0; i < nodes.length; i++)
4095 {
4096 if (!L.globals.defaultNode)
4097 {
4098 var v = L.getHash('view');
4099 if (!v || v == nodes[i].view)
4100 L.globals.defaultNode = nodes[i];
4101 }
4102
4103 var item = $('<li />')
4104 .append($('<a />')
4105 .attr('href', '#')
4106 .text(L.tr(nodes[i].title)))
4107 .appendTo(list);
4108
4109 if (nodes[i].childs && level < max)
4110 {
4111 item.addClass('dropdown');
4112
4113 item.find('a')
4114 .addClass('dropdown-toggle')
4115 .attr('data-toggle', 'dropdown')
4116 .append('<b class="caret"></b>');
4117
4118 item.append(this._render(nodes[i].childs, level + 1));
4119 }
4120 else
4121 {
4122 item.find('a').click(nodes[i].view, this._onclick);
4123 }
4124 }
4125
4126 return list.get(0);
4127 },
4128
4129 render: function(min, max)
4130 {
4131 var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes;
4132 return this._render(top.childs, 0, min, max);
4133 },
4134
4135 getNode: function(path, max)
4136 {
4137 var p = path.split(/\//);
4138 var n = this._nodes;
4139
4140 if (typeof(max) == 'undefined')
4141 max = p.length;
4142
4143 for (var i = 0; i < max; i++)
4144 {
4145 if (!n.childs[p[i]])
4146 return undefined;
4147
4148 n = n.childs[p[i]];
4149 }
4150
4151 return n;
4152 }
4153 });
4154
4155 this.ui.table = this.ui.AbstractWidget.extend({
4156 init: function()
4157 {
4158 this._rows = [ ];
4159 },
4160
4161 row: function(values)
4162 {
4163 if ($.isArray(values))
4164 {
4165 this._rows.push(values);
4166 }
4167 else if ($.isPlainObject(values))
4168 {
4169 var v = [ ];
4170 for (var i = 0; i < this.options.columns.length; i++)
4171 {
4172 var col = this.options.columns[i];
4173
4174 if (typeof col.key == 'string')
4175 v.push(values[col.key]);
4176 else
4177 v.push(null);
4178 }
4179 this._rows.push(v);
4180 }
4181 },
4182
4183 rows: function(rows)
4184 {
4185 for (var i = 0; i < rows.length; i++)
4186 this.row(rows[i]);
4187 },
4188
4189 render: function(id)
4190 {
4191 var fieldset = document.createElement('fieldset');
4192 fieldset.className = 'cbi-section';
4193
4194 if (this.options.caption)
4195 {
4196 var legend = document.createElement('legend');
4197 $(legend).append(this.options.caption);
4198 fieldset.appendChild(legend);
4199 }
4200
4201 var table = document.createElement('table');
4202 table.className = 'table table-condensed table-hover';
4203
4204 var has_caption = false;
4205 var has_description = false;
4206
4207 for (var i = 0; i < this.options.columns.length; i++)
4208 if (this.options.columns[i].caption)
4209 {
4210 has_caption = true;
4211 break;
4212 }
4213 else if (this.options.columns[i].description)
4214 {
4215 has_description = true;
4216 break;
4217 }
4218
4219 if (has_caption)
4220 {
4221 var tr = table.insertRow(-1);
4222 tr.className = 'cbi-section-table-titles';
4223
4224 for (var i = 0; i < this.options.columns.length; i++)
4225 {
4226 var col = this.options.columns[i];
4227 var th = document.createElement('th');
4228 th.className = 'cbi-section-table-cell';
4229
4230 tr.appendChild(th);
4231
4232 if (col.width)
4233 th.style.width = col.width;
4234
4235 if (col.align)
4236 th.style.textAlign = col.align;
4237
4238 if (col.caption)
4239 $(th).append(col.caption);
4240 }
4241 }
4242
4243 if (has_description)
4244 {
4245 var tr = table.insertRow(-1);
4246 tr.className = 'cbi-section-table-descr';
4247
4248 for (var i = 0; i < this.options.columns.length; i++)
4249 {
4250 var col = this.options.columns[i];
4251 var th = document.createElement('th');
4252 th.className = 'cbi-section-table-cell';
4253
4254 tr.appendChild(th);
4255
4256 if (col.width)
4257 th.style.width = col.width;
4258
4259 if (col.align)
4260 th.style.textAlign = col.align;
4261
4262 if (col.description)
4263 $(th).append(col.description);
4264 }
4265 }
4266
4267 if (this._rows.length == 0)
4268 {
4269 if (this.options.placeholder)
4270 {
4271 var tr = table.insertRow(-1);
4272 var td = tr.insertCell(-1);
4273 td.className = 'cbi-section-table-cell';
4274
4275 td.colSpan = this.options.columns.length;
4276 $(td).append(this.options.placeholder);
4277 }
4278 }
4279 else
4280 {
4281 for (var i = 0; i < this._rows.length; i++)
4282 {
4283 var tr = table.insertRow(-1);
4284
4285 for (var j = 0; j < this.options.columns.length; j++)
4286 {
4287 var col = this.options.columns[j];
4288 var td = tr.insertCell(-1);
4289
4290 var val = this._rows[i][j];
4291
4292 if (typeof(val) == 'undefined')
4293 val = col.placeholder;
4294
4295 if (typeof(val) == 'undefined')
4296 val = '';
4297
4298 if (col.width)
4299 td.style.width = col.width;
4300
4301 if (col.align)
4302 td.style.textAlign = col.align;
4303
4304 if (typeof col.format == 'string')
4305 $(td).append(col.format.format(val));
4306 else if (typeof col.format == 'function')
4307 $(td).append(col.format(val, i));
4308 else
4309 $(td).append(val);
4310 }
4311 }
4312 }
4313
4314 this._rows = [ ];
4315 fieldset.appendChild(table);
4316
4317 return fieldset;
4318 }
4319 });
4320
4321 this.ui.progress = this.ui.AbstractWidget.extend({
4322 render: function()
4323 {
4324 var vn = parseInt(this.options.value) || 0;
4325 var mn = parseInt(this.options.max) || 100;
4326 var pc = Math.floor((100 / mn) * vn);
4327
4328 var text;
4329
4330 if (typeof(this.options.format) == 'string')
4331 text = this.options.format.format(this.options.value, this.options.max, pc);
4332 else if (typeof(this.options.format) == 'function')
4333 text = this.options.format(pc);
4334 else
4335 text = '%.2f%%'.format(pc);
4336
4337 return $('<div />')
4338 .addClass('progress')
4339 .append($('<div />')
4340 .addClass('progress-bar')
4341 .addClass('progress-bar-info')
4342 .css('width', pc + '%'))
4343 .append($('<small />')
4344 .text(text));
4345 }
4346 });
4347
4348 this.ui.devicebadge = this.ui.AbstractWidget.extend({
4349 render: function()
4350 {
4351 var l2dev = this.options.l2_device || this.options.device;
4352 var l3dev = this.options.l3_device;
4353 var dev = l3dev || l2dev || '?';
4354
4355 var span = document.createElement('span');
4356 span.className = 'badge';
4357
4358 if (typeof(this.options.signal) == 'number' ||
4359 typeof(this.options.noise) == 'number')
4360 {
4361 var r = 'none';
4362 if (typeof(this.options.signal) != 'undefined' &&
4363 typeof(this.options.noise) != 'undefined')
4364 {
4365 var q = (-1 * (this.options.noise - this.options.signal)) / 5;
4366 if (q < 1)
4367 r = '0';
4368 else if (q < 2)
4369 r = '0-25';
4370 else if (q < 3)
4371 r = '25-50';
4372 else if (q < 4)
4373 r = '50-75';
4374 else
4375 r = '75-100';
4376 }
4377
4378 span.appendChild(document.createElement('img'));
4379 span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png';
4380
4381 if (r == 'none')
4382 span.title = L.tr('No signal');
4383 else
4384 span.title = '%s: %d %s / %s: %d %s'.format(
4385 L.tr('Signal'), this.options.signal, L.tr('dBm'),
4386 L.tr('Noise'), this.options.noise, L.tr('dBm')
4387 );
4388 }
4389 else
4390 {
4391 var type = 'ethernet';
4392 var desc = L.tr('Ethernet device');
4393
4394 if (l3dev != l2dev)
4395 {
4396 type = 'tunnel';
4397 desc = L.tr('Tunnel interface');
4398 }
4399 else if (dev.indexOf('br-') == 0)
4400 {
4401 type = 'bridge';
4402 desc = L.tr('Bridge');
4403 }
4404 else if (dev.indexOf('.') > 0)
4405 {
4406 type = 'vlan';
4407 desc = L.tr('VLAN interface');
4408 }
4409 else if (dev.indexOf('wlan') == 0 ||
4410 dev.indexOf('ath') == 0 ||
4411 dev.indexOf('wl') == 0)
4412 {
4413 type = 'wifi';
4414 desc = L.tr('Wireless Network');
4415 }
4416
4417 span.appendChild(document.createElement('img'));
4418 span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
4419 span.title = desc;
4420 }
4421
4422 $(span).append(' ');
4423 $(span).append(dev);
4424
4425 return span;
4426 }
4427 });
4428
4429 var type = function(f, l)
4430 {
4431 f.message = l;
4432 return f;
4433 };
4434
4435 this.cbi = {
4436 validation: {
4437 i18n: function(msg)
4438 {
4439 L.cbi.validation.message = L.tr(msg);
4440 },
4441
4442 compile: function(code)
4443 {
4444 var pos = 0;
4445 var esc = false;
4446 var depth = 0;
4447 var types = L.cbi.validation.types;
4448 var stack = [ ];
4449
4450 code += ',';
4451
4452 for (var i = 0; i < code.length; i++)
4453 {
4454 if (esc)
4455 {
4456 esc = false;
4457 continue;
4458 }
4459
4460 switch (code.charCodeAt(i))
4461 {
4462 case 92:
4463 esc = true;
4464 break;
4465
4466 case 40:
4467 case 44:
4468 if (depth <= 0)
4469 {
4470 if (pos < i)
4471 {
4472 var label = code.substring(pos, i);
4473 label = label.replace(/\\(.)/g, '$1');
4474 label = label.replace(/^[ \t]+/g, '');
4475 label = label.replace(/[ \t]+$/g, '');
4476
4477 if (label && !isNaN(label))
4478 {
4479 stack.push(parseFloat(label));
4480 }
4481 else if (label.match(/^(['"]).*\1$/))
4482 {
4483 stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
4484 }
4485 else if (typeof types[label] == 'function')
4486 {
4487 stack.push(types[label]);
4488 stack.push([ ]);
4489 }
4490 else
4491 {
4492 throw "Syntax error, unhandled token '"+label+"'";
4493 }
4494 }
4495 pos = i+1;
4496 }
4497 depth += (code.charCodeAt(i) == 40);
4498 break;
4499
4500 case 41:
4501 if (--depth <= 0)
4502 {
4503 if (typeof stack[stack.length-2] != 'function')
4504 throw "Syntax error, argument list follows non-function";
4505
4506 stack[stack.length-1] =
4507 L.cbi.validation.compile(code.substring(pos, i));
4508
4509 pos = i+1;
4510 }
4511 break;
4512 }
4513 }
4514
4515 return stack;
4516 }
4517 }
4518 };
4519
4520 var validation = this.cbi.validation;
4521
4522 validation.types = {
4523 'integer': function()
4524 {
4525 if (this.match(/^-?[0-9]+$/) != null)
4526 return true;
4527
4528 validation.i18n('Must be a valid integer');
4529 return false;
4530 },
4531
4532 'uinteger': function()
4533 {
4534 if (validation.types['integer'].apply(this) && (this >= 0))
4535 return true;
4536
4537 validation.i18n('Must be a positive integer');
4538 return false;
4539 },
4540
4541 'float': function()
4542 {
4543 if (!isNaN(parseFloat(this)))
4544 return true;
4545
4546 validation.i18n('Must be a valid number');
4547 return false;
4548 },
4549
4550 'ufloat': function()
4551 {
4552 if (validation.types['float'].apply(this) && (this >= 0))
4553 return true;
4554
4555 validation.i18n('Must be a positive number');
4556 return false;
4557 },
4558
4559 'ipaddr': function()
4560 {
4561 if (validation.types['ip4addr'].apply(this) ||
4562 validation.types['ip6addr'].apply(this))
4563 return true;
4564
4565 validation.i18n('Must be a valid IP address');
4566 return false;
4567 },
4568
4569 'ip4addr': function()
4570 {
4571 if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/))
4572 {
4573 if ((RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
4574 (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
4575 (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
4576 (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
4577 ((RegExp.$6.indexOf('.') < 0)
4578 ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32))
4579 : (validation.types['ip4addr'].apply(RegExp.$6))))
4580 return true;
4581 }
4582
4583 validation.i18n('Must be a valid IPv4 address');
4584 return false;
4585 },
4586
4587 'ip6addr': function()
4588 {
4589 if (this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/))
4590 {
4591 if (!RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)))
4592 {
4593 var addr = RegExp.$1;
4594
4595 if (addr == '::')
4596 {
4597 return true;
4598 }
4599
4600 if (addr.indexOf('.') > 0)
4601 {
4602 var off = addr.lastIndexOf(':');
4603
4604 if (!(off && validation.types['ip4addr'].apply(addr.substr(off+1))))
4605 {
4606 validation.i18n('Must be a valid IPv6 address');
4607 return false;
4608 }
4609
4610 addr = addr.substr(0, off) + ':0:0';
4611 }
4612
4613 if (addr.indexOf('::') >= 0)
4614 {
4615 var colons = 0;
4616 var fill = '0';
4617
4618 for (var i = 1; i < (addr.length-1); i++)
4619 if (addr.charAt(i) == ':')
4620 colons++;
4621
4622 if (colons > 7)
4623 {
4624 validation.i18n('Must be a valid IPv6 address');
4625 return false;
4626 }
4627
4628 for (var i = 0; i < (7 - colons); i++)
4629 fill += ':0';
4630
4631 if (addr.match(/^(.*?)::(.*?)$/))
4632 addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
4633 (RegExp.$2 ? ':' + RegExp.$2 : '');
4634 }
4635
4636 if (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null)
4637 return true;
4638
4639 validation.i18n('Must be a valid IPv6 address');
4640 return false;
4641 }
4642 }
4643
4644 validation.i18n('Must be a valid IPv6 address');
4645 return false;
4646 },
4647
4648 'port': function()
4649 {
4650 if (validation.types['integer'].apply(this) &&
4651 (this >= 0) && (this <= 65535))
4652 return true;
4653
4654 validation.i18n('Must be a valid port number');
4655 return false;
4656 },
4657
4658 'portrange': function()
4659 {
4660 if (this.match(/^(\d+)-(\d+)$/))
4661 {
4662 var p1 = RegExp.$1;
4663 var p2 = RegExp.$2;
4664
4665 if (validation.types['port'].apply(p1) &&
4666 validation.types['port'].apply(p2) &&
4667 (parseInt(p1) <= parseInt(p2)))
4668 return true;
4669 }
4670 else if (validation.types['port'].apply(this))
4671 {
4672 return true;
4673 }
4674
4675 validation.i18n('Must be a valid port range');
4676 return false;
4677 },
4678
4679 'macaddr': function()
4680 {
4681 if (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null)
4682 return true;
4683
4684 validation.i18n('Must be a valid MAC address');
4685 return false;
4686 },
4687
4688 'host': function()
4689 {
4690 if (validation.types['hostname'].apply(this) ||
4691 validation.types['ipaddr'].apply(this))
4692 return true;
4693
4694 validation.i18n('Must be a valid hostname or IP address');
4695 return false;
4696 },
4697
4698 'hostname': function()
4699 {
4700 if ((this.length <= 253) &&
4701 ((this.match(/^[a-zA-Z0-9]+$/) != null ||
4702 (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
4703 this.match(/[^0-9.]/)))))
4704 return true;
4705
4706 validation.i18n('Must be a valid host name');
4707 return false;
4708 },
4709
4710 'network': function()
4711 {
4712 if (validation.types['uciname'].apply(this) ||
4713 validation.types['host'].apply(this))
4714 return true;
4715
4716 validation.i18n('Must be a valid network name');
4717 return false;
4718 },
4719
4720 'wpakey': function()
4721 {
4722 var v = this;
4723
4724 if ((v.length == 64)
4725 ? (v.match(/^[a-fA-F0-9]{64}$/) != null)
4726 : ((v.length >= 8) && (v.length <= 63)))
4727 return true;
4728
4729 validation.i18n('Must be a valid WPA key');
4730 return false;
4731 },
4732
4733 'wepkey': function()
4734 {
4735 var v = this;
4736
4737 if (v.substr(0,2) == 's:')
4738 v = v.substr(2);
4739
4740 if (((v.length == 10) || (v.length == 26))
4741 ? (v.match(/^[a-fA-F0-9]{10,26}$/) != null)
4742 : ((v.length == 5) || (v.length == 13)))
4743 return true;
4744
4745 validation.i18n('Must be a valid WEP key');
4746 return false;
4747 },
4748
4749 'uciname': function()
4750 {
4751 if (this.match(/^[a-zA-Z0-9_]+$/) != null)
4752 return true;
4753
4754 validation.i18n('Must be a valid UCI identifier');
4755 return false;
4756 },
4757
4758 'range': function(min, max)
4759 {
4760 var val = parseFloat(this);
4761
4762 if (validation.types['integer'].apply(this) &&
4763 !isNaN(min) && !isNaN(max) && ((val >= min) && (val <= max)))
4764 return true;
4765
4766 validation.i18n('Must be a number between %d and %d');
4767 return false;
4768 },
4769
4770 'min': function(min)
4771 {
4772 var val = parseFloat(this);
4773
4774 if (validation.types['integer'].apply(this) &&
4775 !isNaN(min) && !isNaN(val) && (val >= min))
4776 return true;
4777
4778 validation.i18n('Must be a number greater or equal to %d');
4779 return false;
4780 },
4781
4782 'max': function(max)
4783 {
4784 var val = parseFloat(this);
4785
4786 if (validation.types['integer'].apply(this) &&
4787 !isNaN(max) && !isNaN(val) && (val <= max))
4788 return true;
4789
4790 validation.i18n('Must be a number lower or equal to %d');
4791 return false;
4792 },
4793
4794 'rangelength': function(min, max)
4795 {
4796 var val = '' + this;
4797
4798 if (!isNaN(min) && !isNaN(max) &&
4799 (val.length >= min) && (val.length <= max))
4800 return true;
4801
4802 validation.i18n('Must be between %d and %d characters');
4803 return false;
4804 },
4805
4806 'minlength': function(min)
4807 {
4808 var val = '' + this;
4809
4810 if (!isNaN(min) && (val.length >= min))
4811 return true;
4812
4813 validation.i18n('Must be at least %d characters');
4814 return false;
4815 },
4816
4817 'maxlength': function(max)
4818 {
4819 var val = '' + this;
4820
4821 if (!isNaN(max) && (val.length <= max))
4822 return true;
4823
4824 validation.i18n('Must be at most %d characters');
4825 return false;
4826 },
4827
4828 'or': function()
4829 {
4830 var msgs = [ ];
4831
4832 for (var i = 0; i < arguments.length; i += 2)
4833 {
4834 delete validation.message;
4835
4836 if (typeof(arguments[i]) != 'function')
4837 {
4838 if (arguments[i] == this)
4839 return true;
4840 i--;
4841 }
4842 else if (arguments[i].apply(this, arguments[i+1]))
4843 {
4844 return true;
4845 }
4846
4847 if (validation.message)
4848 msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
4849 }
4850
4851 validation.message = msgs.join( L.tr(' - or - '));
4852 return false;
4853 },
4854
4855 'and': function()
4856 {
4857 var msgs = [ ];
4858
4859 for (var i = 0; i < arguments.length; i += 2)
4860 {
4861 delete validation.message;
4862
4863 if (typeof arguments[i] != 'function')
4864 {
4865 if (arguments[i] != this)
4866 return false;
4867 i--;
4868 }
4869 else if (!arguments[i].apply(this, arguments[i+1]))
4870 {
4871 return false;
4872 }
4873
4874 if (validation.message)
4875 msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
4876 }
4877
4878 validation.message = msgs.join(', ');
4879 return true;
4880 },
4881
4882 'neg': function()
4883 {
4884 return validation.types['or'].apply(
4885 this.replace(/^[ \t]*![ \t]*/, ''), arguments);
4886 },
4887
4888 'list': function(subvalidator, subargs)
4889 {
4890 if (typeof subvalidator != 'function')
4891 return false;
4892
4893 var tokens = this.match(/[^ \t]+/g);
4894 for (var i = 0; i < tokens.length; i++)
4895 if (!subvalidator.apply(tokens[i], subargs))
4896 return false;
4897
4898 return true;
4899 },
4900
4901 'phonedigit': function()
4902 {
4903 if (this.match(/^[0-9\*#!\.]+$/) != null)
4904 return true;
4905
4906 validation.i18n('Must be a valid phone number digit');
4907 return false;
4908 },
4909
4910 'string': function()
4911 {
4912 return true;
4913 }
4914 };
4915
4916
4917 this.cbi.AbstractValue = this.ui.AbstractWidget.extend({
4918 init: function(name, options)
4919 {
4920 this.name = name;
4921 this.instance = { };
4922 this.dependencies = [ ];
4923 this.rdependency = { };
4924
4925 this.options = L.defaults(options, {
4926 placeholder: '',
4927 datatype: 'string',
4928 optional: false,
4929 keep: true
4930 });
4931 },
4932
4933 id: function(sid)
4934 {
4935 return this.section.id('field', sid || '__unknown__', this.name);
4936 },
4937
4938 render: function(sid, condensed)
4939 {
4940 var i = this.instance[sid] = { };
4941
4942 i.top = $('<div />');
4943
4944 if (!condensed)
4945 {
4946 i.top.addClass('form-group');
4947
4948 if (typeof(this.options.caption) == 'string')
4949 $('<label />')
4950 .addClass('col-lg-2 control-label')
4951 .attr('for', this.id(sid))
4952 .text(this.options.caption)
4953 .appendTo(i.top);
4954 }
4955
4956 i.error = $('<div />')
4957 .hide()
4958 .addClass('label label-danger');
4959
4960 i.widget = $('<div />')
4961
4962 .append(this.widget(sid))
4963 .append(i.error)
4964 .appendTo(i.top);
4965
4966 if (!condensed)
4967 {
4968 i.widget.addClass('col-lg-5');
4969
4970 $('<div />')
4971 .addClass('col-lg-5')
4972 .text((typeof(this.options.description) == 'string') ? this.options.description : '')
4973 .appendTo(i.top);
4974 }
4975
4976 return i.top;
4977 },
4978
4979 active: function(sid)
4980 {
4981 return (this.instance[sid] && !this.instance[sid].disabled);
4982 },
4983
4984 ucipath: function(sid)
4985 {
4986 return {
4987 config: (this.options.uci_package || this.map.uci_package),
4988 section: (this.options.uci_section || sid),
4989 option: (this.options.uci_option || this.name)
4990 };
4991 },
4992
4993 ucivalue: function(sid)
4994 {
4995 var uci = this.ucipath(sid);
4996 var val = this.map.get(uci.config, uci.section, uci.option);
4997
4998 if (typeof(val) == 'undefined')
4999 return this.options.initial;
5000
5001 return val;
5002 },
5003
5004 formvalue: function(sid)
5005 {
5006 var v = $('#' + this.id(sid)).val();
5007 return (v === '') ? undefined : v;
5008 },
5009
5010 textvalue: function(sid)
5011 {
5012 var v = this.formvalue(sid);
5013
5014 if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
5015 v = this.ucivalue(sid);
5016
5017 if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
5018 v = this.options.placeholder;
5019
5020 if (typeof(v) == 'undefined' || v === '')
5021 return undefined;
5022
5023 if (typeof(v) == 'string' && $.isArray(this.choices))
5024 {
5025 for (var i = 0; i < this.choices.length; i++)
5026 if (v === this.choices[i][0])
5027 return this.choices[i][1];
5028 }
5029 else if (v === true)
5030 return L.tr('yes');
5031 else if (v === false)
5032 return L.tr('no');
5033 else if ($.isArray(v))
5034 return v.join(', ');
5035
5036 return v;
5037 },
5038
5039 changed: function(sid)
5040 {
5041 var a = this.ucivalue(sid);
5042 var b = this.formvalue(sid);
5043
5044 if (typeof(a) != typeof(b))
5045 return true;
5046
5047 if (typeof(a) == 'object')
5048 {
5049 if (a.length != b.length)
5050 return true;
5051
5052 for (var i = 0; i < a.length; i++)
5053 if (a[i] != b[i])
5054 return true;
5055
5056 return false;
5057 }
5058
5059 return (a != b);
5060 },
5061
5062 save: function(sid)
5063 {
5064 var uci = this.ucipath(sid);
5065
5066 if (this.instance[sid].disabled)
5067 {
5068 if (!this.options.keep)
5069 return this.map.set(uci.config, uci.section, uci.option, undefined);
5070
5071 return false;
5072 }
5073
5074 var chg = this.changed(sid);
5075 var val = this.formvalue(sid);
5076
5077 if (chg)
5078 this.map.set(uci.config, uci.section, uci.option, val);
5079
5080 return chg;
5081 },
5082
5083 _ev_validate: function(ev)
5084 {
5085 var d = ev.data;
5086 var rv = true;
5087 var val = d.elem.val();
5088 var vstack = d.vstack;
5089
5090 if (vstack && typeof(vstack[0]) == 'function')
5091 {
5092 delete validation.message;
5093
5094 if ((val.length == 0 && !d.opt))
5095 {
5096 d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
5097 d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
5098
5099 d.inst.error.text(L.tr('Field must not be empty')).show();
5100 rv = false;
5101 }
5102 else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
5103 {
5104 d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
5105 d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
5106
5107 d.inst.error.text(validation.message.format.apply(validation.message, vstack[1])).show();
5108 rv = false;
5109 }
5110 else
5111 {
5112 d.elem.parents('div.form-group, td').first().removeClass('luci2-form-error');
5113 d.elem.parents('div.input-group, div.form-group, td').first().removeClass('has-error');
5114
5115 if (d.multi && d.inst.widget && d.inst.widget.find('input.error, select.error').length > 0)
5116 rv = false;
5117 else
5118 d.inst.error.text('').hide();
5119 }
5120 }
5121
5122 if (rv)
5123 {
5124 for (var field in d.self.rdependency)
5125 d.self.rdependency[field].toggle(d.sid);
5126
5127 d.self.section.tabtoggle(d.sid);
5128 }
5129
5130 return rv;
5131 },
5132
5133 validator: function(sid, elem, multi)
5134 {
5135 var evdata = {
5136 self: this,
5137 sid: sid,
5138 elem: elem,
5139 multi: multi,
5140 inst: this.instance[sid],
5141 opt: this.options.optional
5142 };
5143
5144 if (this.events)
5145 for (var evname in this.events)
5146 elem.on(evname, evdata, this.events[evname]);
5147
5148 if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
5149 return elem;
5150
5151 var vstack;
5152 if (typeof(this.options.datatype) == 'string')
5153 {
5154 try {
5155 evdata.vstack = L.cbi.validation.compile(this.options.datatype);
5156 } catch(e) { };
5157 }
5158 else if (typeof(this.options.datatype) == 'function')
5159 {
5160 var vfunc = this.options.datatype;
5161 evdata.vstack = [ function(elem) {
5162 var rv = vfunc(this, elem);
5163 if (rv !== true)
5164 validation.message = rv;
5165 return (rv === true);
5166 }, [ elem ] ];
5167 }
5168
5169 if (elem.prop('tagName') == 'SELECT')
5170 {
5171 elem.change(evdata, this._ev_validate);
5172 }
5173 else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
5174 {
5175 elem.click(evdata, this._ev_validate);
5176 elem.blur(evdata, this._ev_validate);
5177 }
5178 else
5179 {
5180 elem.keyup(evdata, this._ev_validate);
5181 elem.blur(evdata, this._ev_validate);
5182 }
5183
5184 elem.attr('cbi-validate', true).on('validate', evdata, this._ev_validate);
5185
5186 return elem;
5187 },
5188
5189 validate: function(sid)
5190 {
5191 var i = this.instance[sid];
5192
5193 i.widget.find('[cbi-validate]').trigger('validate');
5194
5195 return (i.disabled || i.error.text() == '');
5196 },
5197
5198 depends: function(d, v, add)
5199 {
5200 var dep;
5201
5202 if ($.isArray(d))
5203 {
5204 dep = { };
5205 for (var i = 0; i < d.length; i++)
5206 {
5207 if (typeof(d[i]) == 'string')
5208 dep[d[i]] = true;
5209 else if (d[i] instanceof L.cbi.AbstractValue)
5210 dep[d[i].name] = true;
5211 }
5212 }
5213 else if (d instanceof L.cbi.AbstractValue)
5214 {
5215 dep = { };
5216 dep[d.name] = (typeof(v) == 'undefined') ? true : v;
5217 }
5218 else if (typeof(d) == 'object')
5219 {
5220 dep = d;
5221 }
5222 else if (typeof(d) == 'string')
5223 {
5224 dep = { };
5225 dep[d] = (typeof(v) == 'undefined') ? true : v;
5226 }
5227
5228 if (!dep || $.isEmptyObject(dep))
5229 return this;
5230
5231 for (var field in dep)
5232 {
5233 var f = this.section.fields[field];
5234 if (f)
5235 f.rdependency[this.name] = this;
5236 else
5237 delete dep[field];
5238 }
5239
5240 if ($.isEmptyObject(dep))
5241 return this;
5242
5243 if (!add || !this.dependencies.length)
5244 this.dependencies.push(dep);
5245 else
5246 for (var i = 0; i < this.dependencies.length; i++)
5247 $.extend(this.dependencies[i], dep);
5248
5249 return this;
5250 },
5251
5252 toggle: function(sid)
5253 {
5254 var d = this.dependencies;
5255 var i = this.instance[sid];
5256
5257 if (!d.length)
5258 return true;
5259
5260 for (var n = 0; n < d.length; n++)
5261 {
5262 var rv = true;
5263
5264 for (var field in d[n])
5265 {
5266 var val = this.section.fields[field].formvalue(sid);
5267 var cmp = d[n][field];
5268
5269 if (typeof(cmp) == 'boolean')
5270 {
5271 if (cmp == (typeof(val) == 'undefined' || val === '' || val === false))
5272 {
5273 rv = false;
5274 break;
5275 }
5276 }
5277 else if (typeof(cmp) == 'string' || typeof(cmp) == 'number')
5278 {
5279 if (val != cmp)
5280 {
5281 rv = false;
5282 break;
5283 }
5284 }
5285 else if (typeof(cmp) == 'function')
5286 {
5287 if (!cmp(val))
5288 {
5289 rv = false;
5290 break;
5291 }
5292 }
5293 else if (cmp instanceof RegExp)
5294 {
5295 if (!cmp.test(val))
5296 {
5297 rv = false;
5298 break;
5299 }
5300 }
5301 }
5302
5303 if (rv)
5304 {
5305 if (i.disabled)
5306 {
5307 i.disabled = false;
5308 i.top.fadeIn();
5309 }
5310
5311 return true;
5312 }
5313 }
5314
5315 if (!i.disabled)
5316 {
5317 i.disabled = true;
5318 i.top.is(':visible') ? i.top.fadeOut() : i.top.hide();
5319 }
5320
5321 return false;
5322 }
5323 });
5324
5325 this.cbi.CheckboxValue = this.cbi.AbstractValue.extend({
5326 widget: function(sid)
5327 {
5328 var o = this.options;
5329
5330 if (typeof(o.enabled) == 'undefined') o.enabled = '1';
5331 if (typeof(o.disabled) == 'undefined') o.disabled = '0';
5332
5333 var i = $('<input />')
5334 .attr('id', this.id(sid))
5335 .attr('type', 'checkbox')
5336 .prop('checked', this.ucivalue(sid));
5337
5338 return $('<div />')
5339 .addClass('checkbox')
5340 .append(this.validator(sid, i));
5341 },
5342
5343 ucivalue: function(sid)
5344 {
5345 var v = this.callSuper('ucivalue', sid);
5346
5347 if (typeof(v) == 'boolean')
5348 return v;
5349
5350 return (v == this.options.enabled);
5351 },
5352
5353 formvalue: function(sid)
5354 {
5355 var v = $('#' + this.id(sid)).prop('checked');
5356
5357 if (typeof(v) == 'undefined')
5358 return !!this.options.initial;
5359
5360 return v;
5361 },
5362
5363 save: function(sid)
5364 {
5365 var uci = this.ucipath(sid);
5366
5367 if (this.instance[sid].disabled)
5368 {
5369 if (!this.options.keep)
5370 return this.map.set(uci.config, uci.section, uci.option, undefined);
5371
5372 return false;
5373 }
5374
5375 var chg = this.changed(sid);
5376 var val = this.formvalue(sid);
5377
5378 if (chg)
5379 {
5380 if (this.options.optional && val == this.options.initial)
5381 this.map.set(uci.config, uci.section, uci.option, undefined);
5382 else
5383 this.map.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
5384 }
5385
5386 return chg;
5387 }
5388 });
5389
5390 this.cbi.InputValue = this.cbi.AbstractValue.extend({
5391 widget: function(sid)
5392 {
5393 var i = $('<input />')
5394 .addClass('form-control')
5395 .attr('id', this.id(sid))
5396 .attr('type', 'text')
5397 .attr('placeholder', this.options.placeholder)
5398 .val(this.ucivalue(sid));
5399
5400 return this.validator(sid, i);
5401 }
5402 });
5403
5404 this.cbi.PasswordValue = this.cbi.AbstractValue.extend({
5405 widget: function(sid)
5406 {
5407 var i = $('<input />')
5408 .addClass('form-control')
5409 .attr('id', this.id(sid))
5410 .attr('type', 'password')
5411 .attr('placeholder', this.options.placeholder)
5412 .val(this.ucivalue(sid));
5413
5414 var t = $('<span />')
5415 .addClass('input-group-btn')
5416 .append(L.ui.button(L.tr('Reveal'), 'default')
5417 .click(function(ev) {
5418 var b = $(this);
5419 var i = b.parent().prev();
5420 var t = i.attr('type');
5421 b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal'));
5422 i.attr('type', (t == 'password') ? 'text' : 'password');
5423 b = i = t = null;
5424 }));
5425
5426 this.validator(sid, i);
5427
5428 return $('<div />')
5429 .addClass('input-group')
5430 .append(i)
5431 .append(t);
5432 }
5433 });
5434
5435 this.cbi.ListValue = this.cbi.AbstractValue.extend({
5436 widget: function(sid)
5437 {
5438 var s = $('<select />')
5439 .addClass('form-control');
5440
5441 if (this.options.optional && !this.has_empty)
5442 $('<option />')
5443 .attr('value', '')
5444 .text(L.tr('-- Please choose --'))
5445 .appendTo(s);
5446
5447 if (this.choices)
5448 for (var i = 0; i < this.choices.length; i++)
5449 $('<option />')
5450 .attr('value', this.choices[i][0])
5451 .text(this.choices[i][1])
5452 .appendTo(s);
5453
5454 s.attr('id', this.id(sid)).val(this.ucivalue(sid));
5455
5456 return this.validator(sid, s);
5457 },
5458
5459 value: function(k, v)
5460 {
5461 if (!this.choices)
5462 this.choices = [ ];
5463
5464 if (k == '')
5465 this.has_empty = true;
5466
5467 this.choices.push([k, v || k]);
5468 return this;
5469 }
5470 });
5471
5472 this.cbi.MultiValue = this.cbi.ListValue.extend({
5473 widget: function(sid)
5474 {
5475 var v = this.ucivalue(sid);
5476 var t = $('<div />').attr('id', this.id(sid));
5477
5478 if (!$.isArray(v))
5479 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5480
5481 var s = { };
5482 for (var i = 0; i < v.length; i++)
5483 s[v[i]] = true;
5484
5485 if (this.choices)
5486 for (var i = 0; i < this.choices.length; i++)
5487 {
5488 $('<label />')
5489 .addClass('checkbox')
5490 .append($('<input />')
5491 .attr('type', 'checkbox')
5492 .attr('value', this.choices[i][0])
5493 .prop('checked', s[this.choices[i][0]]))
5494 .append(this.choices[i][1])
5495 .appendTo(t);
5496 }
5497
5498 return t;
5499 },
5500
5501 formvalue: function(sid)
5502 {
5503 var rv = [ ];
5504 var fields = $('#' + this.id(sid) + ' > label > input');
5505
5506 for (var i = 0; i < fields.length; i++)
5507 if (fields[i].checked)
5508 rv.push(fields[i].getAttribute('value'));
5509
5510 return rv;
5511 },
5512
5513 textvalue: function(sid)
5514 {
5515 var v = this.formvalue(sid);
5516 var c = { };
5517
5518 if (this.choices)
5519 for (var i = 0; i < this.choices.length; i++)
5520 c[this.choices[i][0]] = this.choices[i][1];
5521
5522 var t = [ ];
5523
5524 for (var i = 0; i < v.length; i++)
5525 t.push(c[v[i]] || v[i]);
5526
5527 return t.join(', ');
5528 }
5529 });
5530
5531 this.cbi.ComboBox = this.cbi.AbstractValue.extend({
5532 _change: function(ev)
5533 {
5534 var s = ev.target;
5535 var self = ev.data.self;
5536
5537 if (s.selectedIndex == (s.options.length - 1))
5538 {
5539 ev.data.select.hide();
5540 ev.data.input.show().focus();
5541 ev.data.input.val('');
5542 }
5543 else if (self.options.optional && s.selectedIndex == 0)
5544 {
5545 ev.data.input.val('');
5546 }
5547 else
5548 {
5549 ev.data.input.val(ev.data.select.val());
5550 }
5551
5552 ev.stopPropagation();
5553 },
5554
5555 _blur: function(ev)
5556 {
5557 var seen = false;
5558 var val = this.value;
5559 var self = ev.data.self;
5560
5561 ev.data.select.empty();
5562
5563 if (self.options.optional && !self.has_empty)
5564 $('<option />')
5565 .attr('value', '')
5566 .text(L.tr('-- please choose --'))
5567 .appendTo(ev.data.select);
5568
5569 if (self.choices)
5570 for (var i = 0; i < self.choices.length; i++)
5571 {
5572 if (self.choices[i][0] == val)
5573 seen = true;
5574
5575 $('<option />')
5576 .attr('value', self.choices[i][0])
5577 .text(self.choices[i][1])
5578 .appendTo(ev.data.select);
5579 }
5580
5581 if (!seen && val != '')
5582 $('<option />')
5583 .attr('value', val)
5584 .text(val)
5585 .appendTo(ev.data.select);
5586
5587 $('<option />')
5588 .attr('value', ' ')
5589 .text(L.tr('-- custom --'))
5590 .appendTo(ev.data.select);
5591
5592 ev.data.input.hide();
5593 ev.data.select.val(val).show().blur();
5594 },
5595
5596 _enter: function(ev)
5597 {
5598 if (ev.which != 13)
5599 return true;
5600
5601 ev.preventDefault();
5602 ev.data.self._blur(ev);
5603 return false;
5604 },
5605
5606 widget: function(sid)
5607 {
5608 var d = $('<div />')
5609 .attr('id', this.id(sid));
5610
5611 var t = $('<input />')
5612 .addClass('form-control')
5613 .attr('type', 'text')
5614 .hide()
5615 .appendTo(d);
5616
5617 var s = $('<select />')
5618 .addClass('form-control')
5619 .appendTo(d);
5620
5621 var evdata = {
5622 self: this,
5623 input: t,
5624 select: s
5625 };
5626
5627 s.change(evdata, this._change);
5628 t.blur(evdata, this._blur);
5629 t.keydown(evdata, this._enter);
5630
5631 t.val(this.ucivalue(sid));
5632 t.blur();
5633
5634 this.validator(sid, t);
5635 this.validator(sid, s);
5636
5637 return d;
5638 },
5639
5640 value: function(k, v)
5641 {
5642 if (!this.choices)
5643 this.choices = [ ];
5644
5645 if (k == '')
5646 this.has_empty = true;
5647
5648 this.choices.push([k, v || k]);
5649 return this;
5650 },
5651
5652 formvalue: function(sid)
5653 {
5654 var v = $('#' + this.id(sid)).children('input').val();
5655 return (v == '') ? undefined : v;
5656 }
5657 });
5658
5659 this.cbi.DynamicList = this.cbi.ComboBox.extend({
5660 _redraw: function(focus, add, del, s)
5661 {
5662 var v = s.values || [ ];
5663 delete s.values;
5664
5665 $(s.parent).children('div.input-group').children('input').each(function(i) {
5666 if (i != del)
5667 v.push(this.value || '');
5668 });
5669
5670 $(s.parent).empty();
5671
5672 if (add >= 0)
5673 {
5674 focus = add + 1;
5675 v.splice(focus, 0, '');
5676 }
5677 else if (v.length == 0)
5678 {
5679 focus = 0;
5680 v.push('');
5681 }
5682
5683 for (var i = 0; i < v.length; i++)
5684 {
5685 var evdata = {
5686 sid: s.sid,
5687 self: s.self,
5688 parent: s.parent,
5689 index: i,
5690 remove: ((i+1) < v.length)
5691 };
5692
5693 var btn;
5694 if (evdata.remove)
5695 btn = L.ui.button('–', 'danger').click(evdata, this._btnclick);
5696 else
5697 btn = L.ui.button('+', 'success').click(evdata, this._btnclick);
5698
5699 if (this.choices)
5700 {
5701 var txt = $('<input />')
5702 .addClass('form-control')
5703 .attr('type', 'text')
5704 .hide();
5705
5706 var sel = $('<select />')
5707 .addClass('form-control');
5708
5709 $('<div />')
5710 .addClass('input-group')
5711 .append(txt)
5712 .append(sel)
5713 .append($('<span />')
5714 .addClass('input-group-btn')
5715 .append(btn))
5716 .appendTo(s.parent);
5717
5718 evdata.input = this.validator(s.sid, txt, true);
5719 evdata.select = this.validator(s.sid, sel, true);
5720
5721 sel.change(evdata, this._change);
5722 txt.blur(evdata, this._blur);
5723 txt.keydown(evdata, this._keydown);
5724
5725 txt.val(v[i]);
5726 txt.blur();
5727
5728 if (i == focus || -(i+1) == focus)
5729 sel.focus();
5730
5731 sel = txt = null;
5732 }
5733 else
5734 {
5735 var f = $('<input />')
5736 .attr('type', 'text')
5737 .attr('index', i)
5738 .attr('placeholder', (i == 0) ? this.options.placeholder : '')
5739 .addClass('form-control')
5740 .keydown(evdata, this._keydown)
5741 .keypress(evdata, this._keypress)
5742 .val(v[i]);
5743
5744 $('<div />')
5745 .addClass('input-group')
5746 .append(f)
5747 .append($('<span />')
5748 .addClass('input-group-btn')
5749 .append(btn))
5750 .appendTo(s.parent);
5751
5752 if (i == focus)
5753 {
5754 f.focus();
5755 }
5756 else if (-(i+1) == focus)
5757 {
5758 f.focus();
5759
5760 /* force cursor to end */
5761 var val = f.val();
5762 f.val(' ');
5763 f.val(val);
5764 }
5765
5766 evdata.input = this.validator(s.sid, f, true);
5767
5768 f = null;
5769 }
5770
5771 evdata = null;
5772 }
5773
5774 s = null;
5775 },
5776
5777 _keypress: function(ev)
5778 {
5779 switch (ev.which)
5780 {
5781 /* backspace, delete */
5782 case 8:
5783 case 46:
5784 if (ev.data.input.val() == '')
5785 {
5786 ev.preventDefault();
5787 return false;
5788 }
5789
5790 return true;
5791
5792 /* enter, arrow up, arrow down */
5793 case 13:
5794 case 38:
5795 case 40:
5796 ev.preventDefault();
5797 return false;
5798 }
5799
5800 return true;
5801 },
5802
5803 _keydown: function(ev)
5804 {
5805 var input = ev.data.input;
5806
5807 switch (ev.which)
5808 {
5809 /* backspace, delete */
5810 case 8:
5811 case 46:
5812 if (input.val().length == 0)
5813 {
5814 ev.preventDefault();
5815
5816 var index = ev.data.index;
5817 var focus = index;
5818
5819 if (ev.which == 8)
5820 focus = -focus;
5821
5822 ev.data.self._redraw(focus, -1, index, ev.data);
5823 return false;
5824 }
5825
5826 break;
5827
5828 /* enter */
5829 case 13:
5830 ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
5831 break;
5832
5833 /* arrow up */
5834 case 38:
5835 var prev = input.parent().prevAll('div.input-group:first').children('input');
5836 if (prev.is(':visible'))
5837 prev.focus();
5838 else
5839 prev.next('select').focus();
5840 break;
5841
5842 /* arrow down */
5843 case 40:
5844 var next = input.parent().nextAll('div.input-group:first').children('input');
5845 if (next.is(':visible'))
5846 next.focus();
5847 else
5848 next.next('select').focus();
5849 break;
5850 }
5851
5852 return true;
5853 },
5854
5855 _btnclick: function(ev)
5856 {
5857 if (!this.getAttribute('disabled'))
5858 {
5859 if (ev.data.remove)
5860 {
5861 var index = ev.data.index;
5862 ev.data.self._redraw(-index, -1, index, ev.data);
5863 }
5864 else
5865 {
5866 ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
5867 }
5868 }
5869
5870 return false;
5871 },
5872
5873 widget: function(sid)
5874 {
5875 this.options.optional = true;
5876
5877 var v = this.ucivalue(sid);
5878
5879 if (!$.isArray(v))
5880 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5881
5882 var d = $('<div />')
5883 .attr('id', this.id(sid))
5884 .addClass('cbi-input-dynlist');
5885
5886 this._redraw(NaN, -1, -1, {
5887 self: this,
5888 parent: d[0],
5889 values: v,
5890 sid: sid
5891 });
5892
5893 return d;
5894 },
5895
5896 ucivalue: function(sid)
5897 {
5898 var v = this.callSuper('ucivalue', sid);
5899
5900 if (!$.isArray(v))
5901 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5902
5903 return v;
5904 },
5905
5906 formvalue: function(sid)
5907 {
5908 var rv = [ ];
5909 var fields = $('#' + this.id(sid) + ' input');
5910
5911 for (var i = 0; i < fields.length; i++)
5912 if (typeof(fields[i].value) == 'string' && fields[i].value.length)
5913 rv.push(fields[i].value);
5914
5915 return rv;
5916 }
5917 });
5918
5919 this.cbi.DummyValue = this.cbi.AbstractValue.extend({
5920 widget: function(sid)
5921 {
5922 return $('<div />')
5923 .addClass('form-control-static')
5924 .attr('id', this.id(sid))
5925 .html(this.ucivalue(sid));
5926 },
5927
5928 formvalue: function(sid)
5929 {
5930 return this.ucivalue(sid);
5931 }
5932 });
5933
5934 this.cbi.ButtonValue = this.cbi.AbstractValue.extend({
5935 widget: function(sid)
5936 {
5937 this.options.optional = true;
5938
5939 var btn = $('<button />')
5940 .addClass('btn btn-default')
5941 .attr('id', this.id(sid))
5942 .attr('type', 'button')
5943 .text(this.label('text'));
5944
5945 return this.validator(sid, btn);
5946 }
5947 });
5948
5949 this.cbi.NetworkList = this.cbi.AbstractValue.extend({
5950 load: function(sid)
5951 {
5952 return L.NetworkModel.init();
5953 },
5954
5955 _device_icon: function(dev)
5956 {
5957 return $('<img />')
5958 .attr('src', dev.icon())
5959 .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'));
5960 },
5961
5962 widget: function(sid)
5963 {
5964 var id = this.id(sid);
5965 var ul = $('<ul />')
5966 .attr('id', id)
5967 .addClass('list-unstyled');
5968
5969 var itype = this.options.multiple ? 'checkbox' : 'radio';
5970 var value = this.ucivalue(sid);
5971 var check = { };
5972
5973 if (!this.options.multiple)
5974 check[value] = true;
5975 else
5976 for (var i = 0; i < value.length; i++)
5977 check[value[i]] = true;
5978
5979 var interfaces = L.NetworkModel.getInterfaces();
5980
5981 for (var i = 0; i < interfaces.length; i++)
5982 {
5983 var iface = interfaces[i];
5984 var badge = $('<span />')
5985 .addClass('badge')
5986 .text('%s: '.format(iface.name()));
5987
5988 var dev = iface.getDevice();
5989 var subdevs = iface.getSubdevices();
5990
5991 if (subdevs.length)
5992 for (var j = 0; j < subdevs.length; j++)
5993 badge.append(this._device_icon(subdevs[j]));
5994 else if (dev)
5995 badge.append(this._device_icon(dev));
5996 else
5997 badge.append($('<em />').text(L.tr('(No devices attached)')));
5998
5999 $('<li />')
6000 .append($('<label />')
6001 .addClass(itype + ' inline')
6002 .append($('<input />')
6003 .attr('name', itype + id)
6004 .attr('type', itype)
6005 .attr('value', iface.name())
6006 .prop('checked', !!check[iface.name()]))
6007 .append(badge))
6008 .appendTo(ul);
6009 }
6010
6011 if (!this.options.multiple)
6012 {
6013 $('<li />')
6014 .append($('<label />')
6015 .addClass(itype + ' inline text-muted')
6016 .append($('<input />')
6017 .attr('name', itype + id)
6018 .attr('type', itype)
6019 .attr('value', '')
6020 .prop('checked', $.isEmptyObject(check)))
6021 .append(L.tr('unspecified')))
6022 .appendTo(ul);
6023 }
6024
6025 return ul;
6026 },
6027
6028 ucivalue: function(sid)
6029 {
6030 var v = this.callSuper('ucivalue', sid);
6031
6032 if (!this.options.multiple)
6033 {
6034 if ($.isArray(v))
6035 {
6036 return v[0];
6037 }
6038 else if (typeof(v) == 'string')
6039 {
6040 v = v.match(/\S+/);
6041 return v ? v[0] : undefined;
6042 }
6043
6044 return v;
6045 }
6046 else
6047 {
6048 if (typeof(v) == 'string')
6049 v = v.match(/\S+/g);
6050
6051 return v || [ ];
6052 }
6053 },
6054
6055 formvalue: function(sid)
6056 {
6057 var inputs = $('#' + this.id(sid) + ' input');
6058
6059 if (!this.options.multiple)
6060 {
6061 for (var i = 0; i < inputs.length; i++)
6062 if (inputs[i].checked && inputs[i].value !== '')
6063 return inputs[i].value;
6064
6065 return undefined;
6066 }
6067
6068 var rv = [ ];
6069
6070 for (var i = 0; i < inputs.length; i++)
6071 if (inputs[i].checked)
6072 rv.push(inputs[i].value);
6073
6074 return rv.length ? rv : undefined;
6075 }
6076 });
6077
6078 this.cbi.DeviceList = this.cbi.NetworkList.extend({
6079 _ev_focus: function(ev)
6080 {
6081 var self = ev.data.self;
6082 var input = $(this);
6083
6084 input.parent().prev().prop('checked', true);
6085 },
6086
6087 _ev_blur: function(ev)
6088 {
6089 ev.which = 10;
6090 ev.data.self._ev_keydown.call(this, ev);
6091 },
6092
6093 _ev_keydown: function(ev)
6094 {
6095 if (ev.which != 10 && ev.which != 13)
6096 return;
6097
6098 var sid = ev.data.sid;
6099 var self = ev.data.self;
6100 var input = $(this);
6101 var ifnames = L.toArray(input.val());
6102
6103 if (!ifnames.length)
6104 return;
6105
6106 L.NetworkModel.createDevice(ifnames[0]);
6107
6108 self._redraw(sid, $('#' + self.id(sid)), ifnames[0]);
6109 },
6110
6111 load: function(sid)
6112 {
6113 return L.NetworkModel.init();
6114 },
6115
6116 _redraw: function(sid, ul, sel)
6117 {
6118 var id = ul.attr('id');
6119 var devs = L.NetworkModel.getDevices();
6120 var iface = L.NetworkModel.getInterface(sid);
6121 var itype = this.options.multiple ? 'checkbox' : 'radio';
6122 var check = { };
6123
6124 if (!sel)
6125 {
6126 for (var i = 0; i < devs.length; i++)
6127 if (devs[i].isInNetwork(iface))
6128 check[devs[i].name()] = true;
6129 }
6130 else
6131 {
6132 if (this.options.multiple)
6133 check = L.toObject(this.formvalue(sid));
6134
6135 check[sel] = true;
6136 }
6137
6138 ul.empty();
6139
6140 for (var i = 0; i < devs.length; i++)
6141 {
6142 var dev = devs[i];
6143
6144 if (dev.isBridge() && this.options.bridges === false)
6145 continue;
6146
6147 if (!dev.isBridgeable() && this.options.multiple)
6148 continue;
6149
6150 var badge = $('<span />')
6151 .addClass('badge')
6152 .append($('<img />').attr('src', dev.icon()))
6153 .append(' %s: %s'.format(dev.name(), dev.description()));
6154
6155 //var ifcs = dev.getInterfaces();
6156 //if (ifcs.length)
6157 //{
6158 // for (var j = 0; j < ifcs.length; j++)
6159 // badge.append((j ? ', ' : ' (') + ifcs[j].name());
6160 //
6161 // badge.append(')');
6162 //}
6163
6164 $('<li />')
6165 .append($('<label />')
6166 .addClass(itype + ' inline')
6167 .append($('<input />')
6168 .attr('name', itype + id)
6169 .attr('type', itype)
6170 .attr('value', dev.name())
6171 .prop('checked', !!check[dev.name()]))
6172 .append(badge))
6173 .appendTo(ul);
6174 }
6175
6176
6177 $('<li />')
6178 .append($('<label />')
6179 .attr('for', 'custom' + id)
6180 .addClass(itype + ' inline')
6181 .append($('<input />')
6182 .attr('name', itype + id)
6183 .attr('type', itype)
6184 .attr('value', ''))
6185 .append($('<span />')
6186 .addClass('badge')
6187 .append($('<input />')
6188 .attr('id', 'custom' + id)
6189 .attr('type', 'text')
6190 .attr('placeholder', L.tr('Custom device …'))
6191 .on('focus', { self: this, sid: sid }, this._ev_focus)
6192 .on('blur', { self: this, sid: sid }, this._ev_blur)
6193 .on('keydown', { self: this, sid: sid }, this._ev_keydown))))
6194 .appendTo(ul);
6195
6196 if (!this.options.multiple)
6197 {
6198 $('<li />')
6199 .append($('<label />')
6200 .addClass(itype + ' inline text-muted')
6201 .append($('<input />')
6202 .attr('name', itype + id)
6203 .attr('type', itype)
6204 .attr('value', '')
6205 .prop('checked', $.isEmptyObject(check)))
6206 .append(L.tr('unspecified')))
6207 .appendTo(ul);
6208 }
6209 },
6210
6211 widget: function(sid)
6212 {
6213 var id = this.id(sid);
6214 var ul = $('<ul />')
6215 .attr('id', id)
6216 .addClass('list-unstyled');
6217
6218 this._redraw(sid, ul);
6219
6220 return ul;
6221 },
6222
6223 save: function(sid)
6224 {
6225 if (this.instance[sid].disabled)
6226 return;
6227
6228 var ifnames = this.formvalue(sid);
6229 //if (!ifnames)
6230 // return;
6231
6232 var iface = L.NetworkModel.getInterface(sid);
6233 if (!iface)
6234 return;
6235
6236 iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]);
6237 }
6238 });
6239
6240
6241 this.cbi.AbstractSection = this.ui.AbstractWidget.extend({
6242 id: function()
6243 {
6244 var s = [ arguments[0], this.map.uci_package, this.uci_type ];
6245
6246 for (var i = 1; i < arguments.length; i++)
6247 s.push(arguments[i].replace(/\./g, '_'));
6248
6249 return s.join('_');
6250 },
6251
6252 option: function(widget, name, options)
6253 {
6254 if (this.tabs.length == 0)
6255 this.tab({ id: '__default__', selected: true });
6256
6257 return this.taboption('__default__', widget, name, options);
6258 },
6259
6260 tab: function(options)
6261 {
6262 if (options.selected)
6263 this.tabs.selected = this.tabs.length;
6264
6265 this.tabs.push({
6266 id: options.id,
6267 caption: options.caption,
6268 description: options.description,
6269 fields: [ ],
6270 li: { }
6271 });
6272 },
6273
6274 taboption: function(tabid, widget, name, options)
6275 {
6276 var tab;
6277 for (var i = 0; i < this.tabs.length; i++)
6278 {
6279 if (this.tabs[i].id == tabid)
6280 {
6281 tab = this.tabs[i];
6282 break;
6283 }
6284 }
6285
6286 if (!tab)
6287 throw 'Cannot append to unknown tab ' + tabid;
6288
6289 var w = widget ? new widget(name, options) : null;
6290
6291 if (!(w instanceof L.cbi.AbstractValue))
6292 throw 'Widget must be an instance of AbstractValue';
6293
6294 w.section = this;
6295 w.map = this.map;
6296
6297 this.fields[name] = w;
6298 tab.fields.push(w);
6299
6300 return w;
6301 },
6302
6303 tabtoggle: function(sid)
6304 {
6305 for (var i = 0; i < this.tabs.length; i++)
6306 {
6307 var tab = this.tabs[i];
6308 var elem = $('#' + this.id('nodetab', sid, tab.id));
6309 var empty = true;
6310
6311 for (var j = 0; j < tab.fields.length; j++)
6312 {
6313 if (tab.fields[j].active(sid))
6314 {
6315 empty = false;
6316 break;
6317 }
6318 }
6319
6320 if (empty && elem.is(':visible'))
6321 elem.fadeOut();
6322 else if (!empty)
6323 elem.fadeIn();
6324 }
6325 },
6326
6327 ucipackages: function(pkg)
6328 {
6329 for (var i = 0; i < this.tabs.length; i++)
6330 for (var j = 0; j < this.tabs[i].fields.length; j++)
6331 if (this.tabs[i].fields[j].options.uci_package)
6332 pkg[this.tabs[i].fields[j].options.uci_package] = true;
6333 },
6334
6335 formvalue: function()
6336 {
6337 var rv = { };
6338
6339 this.sections(function(s) {
6340 var sid = s['.name'];
6341 var sv = rv[sid] || (rv[sid] = { });
6342
6343 for (var i = 0; i < this.tabs.length; i++)
6344 for (var j = 0; j < this.tabs[i].fields.length; j++)
6345 {
6346 var val = this.tabs[i].fields[j].formvalue(sid);
6347 sv[this.tabs[i].fields[j].name] = val;
6348 }
6349 });
6350
6351 return rv;
6352 },
6353
6354 validate_section: function(sid)
6355 {
6356 var inst = this.instance[sid];
6357
6358 var invals = 0;
6359 var badge = $('#' + this.id('teaser', sid)).children('span:first');
6360
6361 for (var i = 0; i < this.tabs.length; i++)
6362 {
6363 var inval = 0;
6364 var stbadge = $('#' + this.id('nodetab', sid, this.tabs[i].id)).children('span:first');
6365
6366 for (var j = 0; j < this.tabs[i].fields.length; j++)
6367 if (!this.tabs[i].fields[j].validate(sid))
6368 inval++;
6369
6370 if (inval > 0)
6371 stbadge.show()
6372 .text(inval)
6373 .attr('title', L.trp('1 Error', '%d Errors', inval).format(inval));
6374 else
6375 stbadge.hide();
6376
6377 invals += inval;
6378 }
6379
6380 if (invals > 0)
6381 badge.show()
6382 .text(invals)
6383 .attr('title', L.trp('1 Error', '%d Errors', invals).format(invals));
6384 else
6385 badge.hide();
6386
6387 return invals;
6388 },
6389
6390 validate: function()
6391 {
6392 var errors = 0;
6393 var as = this.sections();
6394
6395 for (var i = 0; i < as.length; i++)
6396 {
6397 var invals = this.validate_section(as[i]['.name']);
6398
6399 if (invals > 0)
6400 errors += invals;
6401 }
6402
6403 var badge = $('#' + this.id('sectiontab')).children('span:first');
6404
6405 if (errors > 0)
6406 badge.show()
6407 .text(errors)
6408 .attr('title', L.trp('1 Error', '%d Errors', errors).format(errors));
6409 else
6410 badge.hide();
6411
6412 return (errors == 0);
6413 }
6414 });
6415
6416 this.cbi.TypedSection = this.cbi.AbstractSection.extend({
6417 init: function(uci_type, options)
6418 {
6419 this.uci_type = uci_type;
6420 this.options = options;
6421 this.tabs = [ ];
6422 this.fields = { };
6423 this.active_panel = 0;
6424 this.active_tab = { };
6425 },
6426
6427 filter: function(section)
6428 {
6429 return true;
6430 },
6431
6432 sections: function(cb)
6433 {
6434 var s1 = L.uci.sections(this.map.uci_package);
6435 var s2 = [ ];
6436
6437 for (var i = 0; i < s1.length; i++)
6438 if (s1[i]['.type'] == this.uci_type)
6439 if (this.filter(s1[i]))
6440 s2.push(s1[i]);
6441
6442 if (typeof(cb) == 'function')
6443 for (var i = 0; i < s2.length; i++)
6444 cb.call(this, s2[i]);
6445
6446 return s2;
6447 },
6448
6449 add: function(name)
6450 {
6451 return this.map.add(this.map.uci_package, this.uci_type, name);
6452 },
6453
6454 remove: function(sid)
6455 {
6456 return this.map.remove(this.map.uci_package, sid);
6457 },
6458
6459 _ev_add: function(ev)
6460 {
6461 var addb = $(this);
6462 var name = undefined;
6463 var self = ev.data.self;
6464
6465 if (addb.prev().prop('nodeName') == 'INPUT')
6466 name = addb.prev().val();
6467
6468 if (addb.prop('disabled') || name === '')
6469 return;
6470
6471 L.ui.saveScrollTop();
6472
6473 self.active_panel = -1;
6474 self.map.save();
6475
6476 ev.data.sid = self.add(name);
6477 ev.data.type = self.uci_type;
6478 ev.data.name = name;
6479
6480 self.trigger('add', ev);
6481
6482 self.map.redraw();
6483
6484 L.ui.restoreScrollTop();
6485 },
6486
6487 _ev_remove: function(ev)
6488 {
6489 var self = ev.data.self;
6490 var sid = ev.data.sid;
6491
6492 L.ui.saveScrollTop();
6493
6494 self.trigger('remove', ev);
6495
6496 self.map.save();
6497 self.remove(sid);
6498 self.map.redraw();
6499
6500 L.ui.restoreScrollTop();
6501
6502 ev.stopPropagation();
6503 },
6504
6505 _ev_sid: function(ev)
6506 {
6507 var self = ev.data.self;
6508 var text = $(this);
6509 var addb = text.next();
6510 var errt = addb.next();
6511 var name = text.val();
6512
6513 if (!/^[a-zA-Z0-9_]*$/.test(name))
6514 {
6515 errt.text(L.tr('Invalid section name')).show();
6516 text.addClass('error');
6517 addb.prop('disabled', true);
6518 return false;
6519 }
6520
6521 if (L.uci.get(self.map.uci_package, name))
6522 {
6523 errt.text(L.tr('Name already used')).show();
6524 text.addClass('error');
6525 addb.prop('disabled', true);
6526 return false;
6527 }
6528
6529 errt.text('').hide();
6530 text.removeClass('error');
6531 addb.prop('disabled', false);
6532 return true;
6533 },
6534
6535 _ev_tab: function(ev)
6536 {
6537 var self = ev.data.self;
6538 var sid = ev.data.sid;
6539
6540 self.validate();
6541 self.active_tab[sid] = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
6542 },
6543
6544 _ev_panel_collapse: function(ev)
6545 {
6546 var self = ev.data.self;
6547
6548 var this_panel = $(ev.target);
6549 var this_toggle = this_panel.prevAll('[data-toggle="collapse"]:first');
6550
6551 var prev_toggle = $($(ev.delegateTarget).find('[data-toggle="collapse"]:eq(%d)'.format(self.active_panel)));
6552 var prev_panel = $(prev_toggle.attr('data-target'));
6553
6554 prev_panel
6555 .removeClass('in')
6556 .addClass('collapse');
6557
6558 prev_toggle.find('.luci2-section-teaser')
6559 .show()
6560 .children('span:last')
6561 .empty()
6562 .append(self.teaser(prev_panel.attr('data-luci2-sid')));
6563
6564 this_toggle.find('.luci2-section-teaser')
6565 .hide();
6566
6567 self.active_panel = parseInt(this_panel.attr('data-luci2-panel-index'));
6568 self.validate();
6569 },
6570
6571 _ev_panel_open: function(ev)
6572 {
6573 var self = ev.data.self;
6574 var panel = $($(this).attr('data-target'));
6575 var index = parseInt(panel.attr('data-luci2-panel-index'));
6576
6577 if (index == self.active_panel)
6578 ev.stopPropagation();
6579 },
6580
6581 _ev_sort: function(ev)
6582 {
6583 var self = ev.data.self;
6584 var cur_idx = ev.data.index;
6585 var new_idx = cur_idx + (ev.data.up ? -1 : 1);
6586 var s = self.sections();
6587
6588 if (new_idx >= 0 && new_idx < s.length)
6589 {
6590 L.uci.swap(self.map.uci_package, s[cur_idx]['.name'], s[new_idx]['.name']);
6591
6592 self.map.save();
6593 self.map.redraw();
6594 }
6595
6596 ev.stopPropagation();
6597 },
6598
6599 teaser: function(sid)
6600 {
6601 var tf = this.teaser_fields;
6602
6603 if (!tf)
6604 {
6605 tf = this.teaser_fields = [ ];
6606
6607 if ($.isArray(this.options.teasers))
6608 {
6609 for (var i = 0; i < this.options.teasers.length; i++)
6610 {
6611 var f = this.options.teasers[i];
6612 if (f instanceof L.cbi.AbstractValue)
6613 tf.push(f);
6614 else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue)
6615 tf.push(this.fields[f]);
6616 }
6617 }
6618 else
6619 {
6620 for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
6621 for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
6622 tf.push(this.tabs[i].fields[j]);
6623 }
6624 }
6625
6626 var t = '';
6627
6628 for (var i = 0; i < tf.length; i++)
6629 {
6630 if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
6631 continue;
6632
6633 var n = tf[i].options.caption || tf[i].name;
6634 var v = tf[i].textvalue(sid);
6635
6636 if (typeof(v) == 'undefined')
6637 continue;
6638
6639 t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
6640 }
6641
6642 return t;
6643 },
6644
6645 _render_add: function()
6646 {
6647 if (!this.options.addremove)
6648 return null;
6649
6650 var text = L.tr('Add section');
6651 var ttip = L.tr('Create new section...');
6652
6653 if ($.isArray(this.options.add_caption))
6654 text = this.options.add_caption[0], ttip = this.options.add_caption[1];
6655 else if (typeof(this.options.add_caption) == 'string')
6656 text = this.options.add_caption, ttip = '';
6657
6658 var add = $('<div />');
6659
6660 if (this.options.anonymous === false)
6661 {
6662 $('<input />')
6663 .addClass('cbi-input-text')
6664 .attr('type', 'text')
6665 .attr('placeholder', ttip)
6666 .blur({ self: this }, this._ev_sid)
6667 .keyup({ self: this }, this._ev_sid)
6668 .appendTo(add);
6669
6670 $('<img />')
6671 .attr('src', L.globals.resource + '/icons/cbi/add.gif')
6672 .attr('title', text)
6673 .addClass('cbi-button')
6674 .click({ self: this }, this._ev_add)
6675 .appendTo(add);
6676
6677 $('<div />')
6678 .addClass('cbi-value-error')
6679 .hide()
6680 .appendTo(add);
6681 }
6682 else
6683 {
6684 L.ui.button(text, 'success', ttip)
6685 .click({ self: this }, this._ev_add)
6686 .appendTo(add);
6687 }
6688
6689 return add;
6690 },
6691
6692 _render_remove: function(sid, index)
6693 {
6694 if (!this.options.addremove)
6695 return null;
6696
6697 var text = L.tr('Remove');
6698 var ttip = L.tr('Remove this section');
6699
6700 if ($.isArray(this.options.remove_caption))
6701 text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
6702 else if (typeof(this.options.remove_caption) == 'string')
6703 text = this.options.remove_caption, ttip = '';
6704
6705 return L.ui.button(text, 'danger', ttip)
6706 .click({ self: this, sid: sid, index: index }, this._ev_remove);
6707 },
6708
6709 _render_sort: function(sid, index)
6710 {
6711 if (!this.options.sortable)
6712 return null;
6713
6714 var b1 = L.ui.button('↑', 'info', L.tr('Move up'))
6715 .click({ self: this, index: index, up: true }, this._ev_sort);
6716
6717 var b2 = L.ui.button('↓', 'info', L.tr('Move down'))
6718 .click({ self: this, index: index, up: false }, this._ev_sort);
6719
6720 return b1.add(b2);
6721 },
6722
6723 _render_caption: function()
6724 {
6725 return $('<h3 />')
6726 .addClass('panel-title')
6727 .append(this.label('caption') || this.uci_type);
6728 },
6729
6730 _render_description: function()
6731 {
6732 var text = this.label('description');
6733
6734 if (text)
6735 return $('<div />')
6736 .addClass('luci2-section-description')
6737 .text(text);
6738
6739 return null;
6740 },
6741
6742 _render_teaser: function(sid, index)
6743 {
6744 if (this.options.collabsible || this.map.options.collabsible)
6745 {
6746 return $('<div />')
6747 .attr('id', this.id('teaser', sid))
6748 .addClass('luci2-section-teaser well well-sm')
6749 .append($('<span />')
6750 .addClass('badge'))
6751 .append($('<span />'));
6752 }
6753
6754 return null;
6755 },
6756
6757 _render_head: function(condensed)
6758 {
6759 if (condensed)
6760 return null;
6761
6762 return $('<div />')
6763 .addClass('panel-heading')
6764 .append(this._render_caption())
6765 .append(this._render_description());
6766 },
6767
6768 _render_tab_description: function(sid, index, tab_index)
6769 {
6770 var tab = this.tabs[tab_index];
6771
6772 if (typeof(tab.description) == 'string')
6773 {
6774 return $('<div />')
6775 .addClass('cbi-tab-descr')
6776 .text(tab.description);
6777 }
6778
6779 return null;
6780 },
6781
6782 _render_tab_head: function(sid, index, tab_index)
6783 {
6784 var tab = this.tabs[tab_index];
6785 var cur = this.active_tab[sid] || 0;
6786
6787 var tabh = $('<li />')
6788 .append($('<a />')
6789 .attr('id', this.id('nodetab', sid, tab.id))
6790 .attr('href', '#' + this.id('node', sid, tab.id))
6791 .attr('data-toggle', 'tab')
6792 .attr('data-luci2-tab-index', tab_index)
6793 .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ')
6794 .append($('<span />')
6795 .addClass('badge'))
6796 .on('shown.bs.tab', { self: this, sid: sid }, this._ev_tab));
6797
6798 if (cur == tab_index)
6799 tabh.addClass('active');
6800
6801 if (!tab.fields.length)
6802 tabh.hide();
6803
6804 return tabh;
6805 },
6806
6807 _render_tab_body: function(sid, index, tab_index)
6808 {
6809 var tab = this.tabs[tab_index];
6810 var cur = this.active_tab[sid] || 0;
6811
6812 var tabb = $('<div />')
6813 .addClass('tab-pane')
6814 .attr('id', this.id('node', sid, tab.id))
6815 .attr('data-luci2-tab-index', tab_index)
6816 .append(this._render_tab_description(sid, index, tab_index));
6817
6818 if (cur == tab_index)
6819 tabb.addClass('active');
6820
6821 for (var i = 0; i < tab.fields.length; i++)
6822 tabb.append(tab.fields[i].render(sid));
6823
6824 return tabb;
6825 },
6826
6827 _render_section_head: function(sid, index)
6828 {
6829 var head = $('<div />')
6830 .addClass('luci2-section-header')
6831 .append(this._render_teaser(sid, index))
6832 .append($('<div />')
6833 .addClass('btn-group')
6834 .append(this._render_sort(sid, index))
6835 .append(this._render_remove(sid, index)));
6836
6837 if (this.options.collabsible)
6838 {
6839 head.attr('data-toggle', 'collapse')
6840 .attr('data-parent', this.id('sectiongroup'))
6841 .attr('data-target', '#' + this.id('panel', sid))
6842 .on('click', { self: this }, this._ev_panel_open);
6843 }
6844
6845 return head;
6846 },
6847
6848 _render_section_body: function(sid, index)
6849 {
6850 var body = $('<div />')
6851 .attr('id', this.id('panel', sid))
6852 .attr('data-luci2-panel-index', index)
6853 .attr('data-luci2-sid', sid);
6854
6855 if (this.options.collabsible || this.map.options.collabsible)
6856 {
6857 body.addClass('panel-collapse collapse');
6858
6859 if (index == this.active_panel)
6860 body.addClass('in');
6861 }
6862
6863 var tab_heads = $('<ul />')
6864 .addClass('nav nav-tabs');
6865
6866 var tab_bodies = $('<div />')
6867 .addClass('form-horizontal tab-content')
6868 .append(tab_heads);
6869
6870 for (var j = 0; j < this.tabs.length; j++)
6871 {
6872 tab_heads.append(this._render_tab_head(sid, index, j));
6873 tab_bodies.append(this._render_tab_body(sid, index, j));
6874 }
6875
6876 body.append(tab_bodies);
6877
6878 if (this.tabs.length <= 1)
6879 tab_heads.hide();
6880
6881 return body;
6882 },
6883
6884 _render_body: function(condensed)
6885 {
6886 var s = this.sections();
6887
6888 if (this.active_panel < 0)
6889 this.active_panel += s.length;
6890 else if (this.active_panel >= s.length)
6891 this.active_panel = s.length - 1;
6892
6893 var body = $('<ul />')
6894 .addClass('list-group');
6895
6896 if (this.options.collabsible)
6897 {
6898 body.attr('id', this.id('sectiongroup'))
6899 .on('show.bs.collapse', { self: this }, this._ev_panel_collapse);
6900 }
6901
6902 if (s.length == 0)
6903 {
6904 body.append($('<li />')
6905 .addClass('list-group-item text-muted')
6906 .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))
6907 }
6908
6909 for (var i = 0; i < s.length; i++)
6910 {
6911 var sid = s[i]['.name'];
6912 var inst = this.instance[sid] = { tabs: [ ] };
6913
6914 body.append($('<li />')
6915 .addClass('list-group-item')
6916 .append(this._render_section_head(sid, i))
6917 .append(this._render_section_body(sid, i)));
6918 }
6919
6920 return body;
6921 },
6922
6923 render: function(condensed)
6924 {
6925 this.instance = { };
6926
6927 var panel = $('<div />')
6928 .addClass('panel panel-default')
6929 .append(this._render_head(condensed))
6930 .append(this._render_body(condensed));
6931
6932 if (this.options.addremove)
6933 panel.append($('<div />')
6934 .addClass('panel-footer')
6935 .append(this._render_add()));
6936
6937 return panel;
6938 },
6939
6940 finish: function()
6941 {
6942 var s = this.sections();
6943
6944 for (var i = 0; i < s.length; i++)
6945 {
6946 var sid = s[i]['.name'];
6947
6948 this.validate_section(sid);
6949
6950 if (i != this.active_panel)
6951 $('#' + this.id('teaser', sid)).children('span:last')
6952 .append(this.teaser(sid));
6953 else
6954 $('#' + this.id('teaser', sid))
6955 .hide();
6956 }
6957 }
6958 });
6959
6960 this.cbi.TableSection = this.cbi.TypedSection.extend({
6961 _render_table_head: function()
6962 {
6963 var thead = $('<thead />')
6964 .append($('<tr />')
6965 .addClass('cbi-section-table-titles'));
6966
6967 for (var j = 0; j < this.tabs[0].fields.length; j++)
6968 thead.children().append($('<th />')
6969 .addClass('cbi-section-table-cell')
6970 .css('width', this.tabs[0].fields[j].options.width || '')
6971 .append(this.tabs[0].fields[j].label('caption')));
6972
6973 if (this.options.addremove !== false || this.options.sortable)
6974 thead.children().append($('<th />')
6975 .addClass('cbi-section-table-cell')
6976 .text(' '));
6977
6978 return thead;
6979 },
6980
6981 _render_table_row: function(sid, index)
6982 {
6983 var row = $('<tr />')
6984 .attr('data-luci2-sid', sid);
6985
6986 for (var j = 0; j < this.tabs[0].fields.length; j++)
6987 {
6988 row.append($('<td />')
6989 .css('width', this.tabs[0].fields[j].options.width || '')
6990 .append(this.tabs[0].fields[j].render(sid, true)));
6991 }
6992
6993 if (this.options.addremove !== false || this.options.sortable)
6994 {
6995 row.append($('<td />')
6996 .addClass('text-right')
6997 .append($('<div />')
6998 .addClass('btn-group')
6999 .append(this._render_sort(sid, index))
7000 .append(this._render_remove(sid, index))));
7001 }
7002
7003 return row;
7004 },
7005
7006 _render_table_body: function()
7007 {
7008 var s = this.sections();
7009
7010 var tbody = $('<tbody />');
7011
7012 if (s.length == 0)
7013 {
7014 var cols = this.tabs[0].fields.length;
7015
7016 if (this.options.addremove !== false || this.options.sortable)
7017 cols++;
7018
7019 tbody.append($('<tr />')
7020 .append($('<td />')
7021 .addClass('text-muted')
7022 .attr('colspan', cols)
7023 .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
7024 }
7025
7026 for (var i = 0; i < s.length; i++)
7027 {
7028 var sid = s[i]['.name'];
7029 var inst = this.instance[sid] = { tabs: [ ] };
7030
7031 tbody.append(this._render_table_row(sid, i));
7032 }
7033
7034 return tbody;
7035 },
7036
7037 _render_body: function(condensed)
7038 {
7039 return $('<table />')
7040 .addClass('table table-condensed table-hover')
7041 .append(this._render_table_head())
7042 .append(this._render_table_body());
7043 }
7044 });
7045
7046 this.cbi.NamedSection = this.cbi.TypedSection.extend({
7047 sections: function(cb)
7048 {
7049 var sa = [ ];
7050 var sl = L.uci.sections(this.map.uci_package);
7051
7052 for (var i = 0; i < sl.length; i++)
7053 if (sl[i]['.name'] == this.uci_type)
7054 {
7055 sa.push(sl[i]);
7056 break;
7057 }
7058
7059 if (typeof(cb) == 'function' && sa.length > 0)
7060 cb.call(this, sa[0]);
7061
7062 return sa;
7063 }
7064 });
7065
7066 this.cbi.SingleSection = this.cbi.NamedSection.extend({
7067 render: function()
7068 {
7069 this.instance = { };
7070 this.instance[this.uci_type] = { tabs: [ ] };
7071
7072 return this._render_section_body(this.uci_type, 0);
7073 }
7074 });
7075
7076 this.cbi.DummySection = this.cbi.TypedSection.extend({
7077 sections: function(cb)
7078 {
7079 if (typeof(cb) == 'function')
7080 cb.apply(this, [ { '.name': this.uci_type } ]);
7081
7082 return [ { '.name': this.uci_type } ];
7083 }
7084 });
7085
7086 this.cbi.Map = this.ui.AbstractWidget.extend({
7087 init: function(uci_package, options)
7088 {
7089 var self = this;
7090
7091 this.uci_package = uci_package;
7092 this.sections = [ ];
7093 this.options = L.defaults(options, {
7094 save: function() { },
7095 prepare: function() { }
7096 });
7097 },
7098
7099 _load_cb: function()
7100 {
7101 var deferreds = [ L.deferrable(this.options.prepare()) ];
7102
7103 for (var i = 0; i < this.sections.length; i++)
7104 {
7105 for (var f in this.sections[i].fields)
7106 {
7107 if (typeof(this.sections[i].fields[f].load) != 'function')
7108 continue;
7109
7110 var s = this.sections[i].sections();
7111 for (var j = 0; j < s.length; j++)
7112 {
7113 var rv = this.sections[i].fields[f].load(s[j]['.name']);
7114 if (L.isDeferred(rv))
7115 deferreds.push(rv);
7116 }
7117 }
7118 }
7119
7120 return $.when.apply($, deferreds);
7121 },
7122
7123 load: function()
7124 {
7125 var self = this;
7126 var packages = { };
7127
7128 for (var i = 0; i < this.sections.length; i++)
7129 this.sections[i].ucipackages(packages);
7130
7131 packages[this.uci_package] = true;
7132
7133 for (var pkg in packages)
7134 if (!L.uci.writable(pkg))
7135 this.options.readonly = true;
7136
7137 return L.uci.load(L.toArray(packages)).then(function() {
7138 return self._load_cb();
7139 });
7140 },
7141
7142 _ev_tab: function(ev)
7143 {
7144 var self = ev.data.self;
7145
7146 self.validate();
7147 self.active_tab = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
7148 },
7149
7150 _ev_apply: function(ev)
7151 {
7152 var self = ev.data.self;
7153
7154 self.trigger('apply', ev);
7155 },
7156
7157 _ev_save: function(ev)
7158 {
7159 var self = ev.data.self;
7160
7161 self.send().then(function() {
7162 self.trigger('save', ev);
7163 });
7164 },
7165
7166 _ev_reset: function(ev)
7167 {
7168 var self = ev.data.self;
7169
7170 self.trigger('reset', ev);
7171 self.reset();
7172 },
7173
7174 _render_tab_head: function(tab_index)
7175 {
7176 var section = this.sections[tab_index];
7177 var cur = this.active_tab || 0;
7178
7179 var tabh = $('<li />')
7180 .append($('<a />')
7181 .attr('id', section.id('sectiontab'))
7182 .attr('href', '#' + section.id('section'))
7183 .attr('data-toggle', 'tab')
7184 .attr('data-luci2-tab-index', tab_index)
7185 .text(section.label('caption') + ' ')
7186 .append($('<span />')
7187 .addClass('badge'))
7188 .on('shown.bs.tab', { self: this }, this._ev_tab));
7189
7190 if (cur == tab_index)
7191 tabh.addClass('active');
7192
7193 return tabh;
7194 },
7195
7196 _render_tab_body: function(tab_index)
7197 {
7198 var section = this.sections[tab_index];
7199 var desc = section.label('description');
7200 var cur = this.active_tab || 0;
7201
7202 var tabb = $('<div />')
7203 .addClass('tab-pane')
7204 .attr('id', section.id('section'))
7205 .attr('data-luci2-tab-index', tab_index);
7206
7207 if (cur == tab_index)
7208 tabb.addClass('active');
7209
7210 if (desc)
7211 tabb.append($('<p />')
7212 .text(desc));
7213
7214 var s = section.render(this.options.tabbed);
7215
7216 if (this.options.readonly || section.options.readonly)
7217 s.find('input, select, button, img.cbi-button').attr('disabled', true);
7218
7219 tabb.append(s);
7220
7221 return tabb;
7222 },
7223
7224 _render_body: function()
7225 {
7226 var tabs = $('<ul />')
7227 .addClass('nav nav-tabs');
7228
7229 var body = $('<div />')
7230 .append(tabs);
7231
7232 for (var i = 0; i < this.sections.length; i++)
7233 {
7234 tabs.append(this._render_tab_head(i));
7235 body.append(this._render_tab_body(i));
7236 }
7237
7238 if (this.options.tabbed)
7239 body.addClass('tab-content');
7240 else
7241 tabs.hide();
7242
7243 return body;
7244 },
7245
7246 _render_footer: function()
7247 {
7248 var evdata = {
7249 self: this
7250 };
7251
7252 return $('<div />')
7253 .addClass('panel panel-default panel-body text-right')
7254 .append($('<div />')
7255 .addClass('btn-group')
7256 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
7257 .click(evdata, this._ev_apply))
7258 .append(L.ui.button(L.tr('Save'), 'default')
7259 .click(evdata, this._ev_save))
7260 .append(L.ui.button(L.tr('Reset'), 'default')
7261 .click(evdata, this._ev_reset)));
7262 },
7263
7264 render: function()
7265 {
7266 var map = $('<form />');
7267
7268 if (typeof(this.options.caption) == 'string')
7269 map.append($('<h2 />')
7270 .text(this.options.caption));
7271
7272 if (typeof(this.options.description) == 'string')
7273 map.append($('<p />')
7274 .text(this.options.description));
7275
7276 map.append(this._render_body());
7277
7278 if (this.options.pageaction !== false)
7279 map.append(this._render_footer());
7280
7281 return map;
7282 },
7283
7284 finish: function()
7285 {
7286 for (var i = 0; i < this.sections.length; i++)
7287 this.sections[i].finish();
7288
7289 this.validate();
7290 },
7291
7292 redraw: function()
7293 {
7294 this.target.hide().empty().append(this.render());
7295 this.finish();
7296 this.target.show();
7297 },
7298
7299 section: function(widget, uci_type, options)
7300 {
7301 var w = widget ? new widget(uci_type, options) : null;
7302
7303 if (!(w instanceof L.cbi.AbstractSection))
7304 throw 'Widget must be an instance of AbstractSection';
7305
7306 w.map = this;
7307 w.index = this.sections.length;
7308
7309 this.sections.push(w);
7310 return w;
7311 },
7312
7313 formvalue: function()
7314 {
7315 var rv = { };
7316
7317 for (var i = 0; i < this.sections.length; i++)
7318 {
7319 var sids = this.sections[i].formvalue();
7320 for (var sid in sids)
7321 {
7322 var s = rv[sid] || (rv[sid] = { });
7323 $.extend(s, sids[sid]);
7324 }
7325 }
7326
7327 return rv;
7328 },
7329
7330 add: function(conf, type, name)
7331 {
7332 return L.uci.add(conf, type, name);
7333 },
7334
7335 remove: function(conf, sid)
7336 {
7337 return L.uci.remove(conf, sid);
7338 },
7339
7340 get: function(conf, sid, opt)
7341 {
7342 return L.uci.get(conf, sid, opt);
7343 },
7344
7345 set: function(conf, sid, opt, val)
7346 {
7347 return L.uci.set(conf, sid, opt, val);
7348 },
7349
7350 validate: function()
7351 {
7352 var rv = true;
7353
7354 for (var i = 0; i < this.sections.length; i++)
7355 {
7356 if (!this.sections[i].validate())
7357 rv = false;
7358 }
7359
7360 return rv;
7361 },
7362
7363 save: function()
7364 {
7365 var self = this;
7366
7367 if (self.options.readonly)
7368 return L.deferrable();
7369
7370 var deferreds = [ ];
7371
7372 for (var i = 0; i < self.sections.length; i++)
7373 {
7374 if (self.sections[i].options.readonly)
7375 continue;
7376
7377 for (var f in self.sections[i].fields)
7378 {
7379 if (typeof(self.sections[i].fields[f].save) != 'function')
7380 continue;
7381
7382 var s = self.sections[i].sections();
7383 for (var j = 0; j < s.length; j++)
7384 {
7385 var rv = self.sections[i].fields[f].save(s[j]['.name']);
7386 if (L.isDeferred(rv))
7387 deferreds.push(rv);
7388 }
7389 }
7390 }
7391
7392 return $.when.apply($, deferreds).then(function() {
7393 return L.deferrable(self.options.save());
7394 });
7395 },
7396
7397 send: function()
7398 {
7399 if (!this.validate())
7400 return L.deferrable();
7401
7402 var self = this;
7403
7404 L.ui.saveScrollTop();
7405 L.ui.loading(true);
7406
7407 return this.save().then(function() {
7408 return L.uci.save();
7409 }).then(function() {
7410 return L.ui.updateChanges();
7411 }).then(function() {
7412 return self.load();
7413 }).then(function() {
7414 self.redraw();
7415 self = null;
7416
7417 L.ui.loading(false);
7418 L.ui.restoreScrollTop();
7419 });
7420 },
7421
7422 revert: function()
7423 {
7424 var packages = { };
7425
7426 for (var i = 0; i < this.sections.length; i++)
7427 this.sections[i].ucipackages(packages);
7428
7429 packages[this.uci_package] = true;
7430
7431 L.uci.unload(L.toArray(packages));
7432 },
7433
7434 reset: function()
7435 {
7436 var self = this;
7437
7438 self.revert();
7439
7440 return self.insertInto(self.target);
7441 },
7442
7443 insertInto: function(id)
7444 {
7445 var self = this;
7446 self.target = $(id);
7447
7448 L.ui.loading(true);
7449 self.target.hide();
7450
7451 return self.load().then(function() {
7452 self.target.empty().append(self.render());
7453 self.finish();
7454 self.target.show();
7455 self = null;
7456 L.ui.loading(false);
7457 });
7458 }
7459 });
7460
7461 this.cbi.Modal = this.cbi.Map.extend({
7462 _ev_apply: function(ev)
7463 {
7464 var self = ev.data.self;
7465
7466 self.trigger('apply', ev);
7467 },
7468
7469 _ev_save: function(ev)
7470 {
7471 var self = ev.data.self;
7472
7473 self.send().then(function() {
7474 self.trigger('save', ev);
7475 self.close();
7476 });
7477 },
7478
7479 _ev_reset: function(ev)
7480 {
7481 var self = ev.data.self;
7482
7483 self.trigger('close', ev);
7484 self.revert();
7485 self.close();
7486 },
7487
7488 _render_footer: function()
7489 {
7490 var evdata = {
7491 self: this
7492 };
7493
7494 return $('<div />')
7495 .addClass('btn-group')
7496 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
7497 .click(evdata, this._ev_apply))
7498 .append(L.ui.button(L.tr('Save'), 'default')
7499 .click(evdata, this._ev_save))
7500 .append(L.ui.button(L.tr('Cancel'), 'default')
7501 .click(evdata, this._ev_reset));
7502 },
7503
7504 render: function()
7505 {
7506 var modal = L.ui.dialog(this.label('caption'), null, { wide: true });
7507 var map = $('<form />');
7508
7509 var desc = this.label('description');
7510 if (desc)
7511 map.append($('<p />').text(desc));
7512
7513 map.append(this._render_body());
7514
7515 modal.find('.modal-body').append(map);
7516 modal.find('.modal-footer').append(this._render_footer());
7517
7518 return modal;
7519 },
7520
7521 redraw: function()
7522 {
7523 this.render();
7524 this.finish();
7525 },
7526
7527 show: function()
7528 {
7529 var self = this;
7530
7531 L.ui.loading(true);
7532
7533 return self.load().then(function() {
7534 self.render();
7535 self.finish();
7536
7537 L.ui.loading(false);
7538 });
7539 },
7540
7541 close: function()
7542 {
7543 L.ui.dialog(false);
7544 }
7545 });
7546 };