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