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