luci2: add L.NetworkModel.Interface.renderBadge() and use it in L.cbi.NetworkList...
[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 (validation.types['ip4addr'].apply(this) ||
4615 validation.types['ip6addr'].apply(this))
4616 return true;
4617
4618 validation.i18n('Must be a valid IP address');
4619 return false;
4620 },
4621
4622 'ip4addr': function()
4623 {
4624 if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/))
4625 {
4626 if ((RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
4627 (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
4628 (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
4629 (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
4630 ((RegExp.$6.indexOf('.') < 0)
4631 ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32))
4632 : (validation.types['ip4addr'].apply(RegExp.$6))))
4633 return true;
4634 }
4635
4636 validation.i18n('Must be a valid IPv4 address');
4637 return false;
4638 },
4639
4640 'ip6addr': function()
4641 {
4642 if (this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/))
4643 {
4644 if (!RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)))
4645 {
4646 var addr = RegExp.$1;
4647
4648 if (addr == '::')
4649 {
4650 return true;
4651 }
4652
4653 if (addr.indexOf('.') > 0)
4654 {
4655 var off = addr.lastIndexOf(':');
4656
4657 if (!(off && validation.types['ip4addr'].apply(addr.substr(off+1))))
4658 {
4659 validation.i18n('Must be a valid IPv6 address');
4660 return false;
4661 }
4662
4663 addr = addr.substr(0, off) + ':0:0';
4664 }
4665
4666 if (addr.indexOf('::') >= 0)
4667 {
4668 var colons = 0;
4669 var fill = '0';
4670
4671 for (var i = 1; i < (addr.length-1); i++)
4672 if (addr.charAt(i) == ':')
4673 colons++;
4674
4675 if (colons > 7)
4676 {
4677 validation.i18n('Must be a valid IPv6 address');
4678 return false;
4679 }
4680
4681 for (var i = 0; i < (7 - colons); i++)
4682 fill += ':0';
4683
4684 if (addr.match(/^(.*?)::(.*?)$/))
4685 addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
4686 (RegExp.$2 ? ':' + RegExp.$2 : '');
4687 }
4688
4689 if (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null)
4690 return true;
4691
4692 validation.i18n('Must be a valid IPv6 address');
4693 return false;
4694 }
4695 }
4696
4697 validation.i18n('Must be a valid IPv6 address');
4698 return false;
4699 },
4700
4701 'port': function()
4702 {
4703 if (validation.types['integer'].apply(this) &&
4704 (this >= 0) && (this <= 65535))
4705 return true;
4706
4707 validation.i18n('Must be a valid port number');
4708 return false;
4709 },
4710
4711 'portrange': function()
4712 {
4713 if (this.match(/^(\d+)-(\d+)$/))
4714 {
4715 var p1 = RegExp.$1;
4716 var p2 = RegExp.$2;
4717
4718 if (validation.types['port'].apply(p1) &&
4719 validation.types['port'].apply(p2) &&
4720 (parseInt(p1) <= parseInt(p2)))
4721 return true;
4722 }
4723 else if (validation.types['port'].apply(this))
4724 {
4725 return true;
4726 }
4727
4728 validation.i18n('Must be a valid port range');
4729 return false;
4730 },
4731
4732 'macaddr': function()
4733 {
4734 if (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null)
4735 return true;
4736
4737 validation.i18n('Must be a valid MAC address');
4738 return false;
4739 },
4740
4741 'host': function()
4742 {
4743 if (validation.types['hostname'].apply(this) ||
4744 validation.types['ipaddr'].apply(this))
4745 return true;
4746
4747 validation.i18n('Must be a valid hostname or IP address');
4748 return false;
4749 },
4750
4751 'hostname': function()
4752 {
4753 if ((this.length <= 253) &&
4754 ((this.match(/^[a-zA-Z0-9]+$/) != null ||
4755 (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
4756 this.match(/[^0-9.]/)))))
4757 return true;
4758
4759 validation.i18n('Must be a valid host name');
4760 return false;
4761 },
4762
4763 'network': function()
4764 {
4765 if (validation.types['uciname'].apply(this) ||
4766 validation.types['host'].apply(this))
4767 return true;
4768
4769 validation.i18n('Must be a valid network name');
4770 return false;
4771 },
4772
4773 'wpakey': function()
4774 {
4775 var v = this;
4776
4777 if ((v.length == 64)
4778 ? (v.match(/^[a-fA-F0-9]{64}$/) != null)
4779 : ((v.length >= 8) && (v.length <= 63)))
4780 return true;
4781
4782 validation.i18n('Must be a valid WPA key');
4783 return false;
4784 },
4785
4786 'wepkey': function()
4787 {
4788 var v = this;
4789
4790 if (v.substr(0,2) == 's:')
4791 v = v.substr(2);
4792
4793 if (((v.length == 10) || (v.length == 26))
4794 ? (v.match(/^[a-fA-F0-9]{10,26}$/) != null)
4795 : ((v.length == 5) || (v.length == 13)))
4796 return true;
4797
4798 validation.i18n('Must be a valid WEP key');
4799 return false;
4800 },
4801
4802 'uciname': function()
4803 {
4804 if (this.match(/^[a-zA-Z0-9_]+$/) != null)
4805 return true;
4806
4807 validation.i18n('Must be a valid UCI identifier');
4808 return false;
4809 },
4810
4811 'range': function(min, max)
4812 {
4813 var val = parseFloat(this);
4814
4815 if (validation.types['integer'].apply(this) &&
4816 !isNaN(min) && !isNaN(max) && ((val >= min) && (val <= max)))
4817 return true;
4818
4819 validation.i18n('Must be a number between %d and %d');
4820 return false;
4821 },
4822
4823 'min': function(min)
4824 {
4825 var val = parseFloat(this);
4826
4827 if (validation.types['integer'].apply(this) &&
4828 !isNaN(min) && !isNaN(val) && (val >= min))
4829 return true;
4830
4831 validation.i18n('Must be a number greater or equal to %d');
4832 return false;
4833 },
4834
4835 'max': function(max)
4836 {
4837 var val = parseFloat(this);
4838
4839 if (validation.types['integer'].apply(this) &&
4840 !isNaN(max) && !isNaN(val) && (val <= max))
4841 return true;
4842
4843 validation.i18n('Must be a number lower or equal to %d');
4844 return false;
4845 },
4846
4847 'rangelength': function(min, max)
4848 {
4849 var val = '' + this;
4850
4851 if (!isNaN(min) && !isNaN(max) &&
4852 (val.length >= min) && (val.length <= max))
4853 return true;
4854
4855 validation.i18n('Must be between %d and %d characters');
4856 return false;
4857 },
4858
4859 'minlength': function(min)
4860 {
4861 var val = '' + this;
4862
4863 if (!isNaN(min) && (val.length >= min))
4864 return true;
4865
4866 validation.i18n('Must be at least %d characters');
4867 return false;
4868 },
4869
4870 'maxlength': function(max)
4871 {
4872 var val = '' + this;
4873
4874 if (!isNaN(max) && (val.length <= max))
4875 return true;
4876
4877 validation.i18n('Must be at most %d characters');
4878 return false;
4879 },
4880
4881 'or': function()
4882 {
4883 var msgs = [ ];
4884
4885 for (var i = 0; i < arguments.length; i += 2)
4886 {
4887 delete validation.message;
4888
4889 if (typeof(arguments[i]) != 'function')
4890 {
4891 if (arguments[i] == this)
4892 return true;
4893 i--;
4894 }
4895 else if (arguments[i].apply(this, arguments[i+1]))
4896 {
4897 return true;
4898 }
4899
4900 if (validation.message)
4901 msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
4902 }
4903
4904 validation.message = msgs.join( L.tr(' - or - '));
4905 return false;
4906 },
4907
4908 'and': function()
4909 {
4910 var msgs = [ ];
4911
4912 for (var i = 0; i < arguments.length; i += 2)
4913 {
4914 delete validation.message;
4915
4916 if (typeof arguments[i] != 'function')
4917 {
4918 if (arguments[i] != this)
4919 return false;
4920 i--;
4921 }
4922 else if (!arguments[i].apply(this, arguments[i+1]))
4923 {
4924 return false;
4925 }
4926
4927 if (validation.message)
4928 msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
4929 }
4930
4931 validation.message = msgs.join(', ');
4932 return true;
4933 },
4934
4935 'neg': function()
4936 {
4937 return validation.types['or'].apply(
4938 this.replace(/^[ \t]*![ \t]*/, ''), arguments);
4939 },
4940
4941 'list': function(subvalidator, subargs)
4942 {
4943 if (typeof subvalidator != 'function')
4944 return false;
4945
4946 var tokens = this.match(/[^ \t]+/g);
4947 for (var i = 0; i < tokens.length; i++)
4948 if (!subvalidator.apply(tokens[i], subargs))
4949 return false;
4950
4951 return true;
4952 },
4953
4954 'phonedigit': function()
4955 {
4956 if (this.match(/^[0-9\*#!\.]+$/) != null)
4957 return true;
4958
4959 validation.i18n('Must be a valid phone number digit');
4960 return false;
4961 },
4962
4963 'string': function()
4964 {
4965 return true;
4966 }
4967 };
4968
4969
4970 this.cbi.AbstractValue = this.ui.AbstractWidget.extend({
4971 init: function(name, options)
4972 {
4973 this.name = name;
4974 this.instance = { };
4975 this.dependencies = [ ];
4976 this.rdependency = { };
4977
4978 this.options = L.defaults(options, {
4979 placeholder: '',
4980 datatype: 'string',
4981 optional: false,
4982 keep: true
4983 });
4984 },
4985
4986 id: function(sid)
4987 {
4988 return this.section.id('field', sid || '__unknown__', this.name);
4989 },
4990
4991 render: function(sid, condensed)
4992 {
4993 var i = this.instance[sid] = { };
4994
4995 i.top = $('<div />');
4996
4997 if (!condensed)
4998 {
4999 i.top.addClass('form-group');
5000
5001 if (typeof(this.options.caption) == 'string')
5002 $('<label />')
5003 .addClass('col-lg-2 control-label')
5004 .attr('for', this.id(sid))
5005 .text(this.options.caption)
5006 .appendTo(i.top);
5007 }
5008
5009 i.error = $('<div />')
5010 .hide()
5011 .addClass('label label-danger');
5012
5013 i.widget = $('<div />')
5014
5015 .append(this.widget(sid))
5016 .append(i.error)
5017 .appendTo(i.top);
5018
5019 if (!condensed)
5020 {
5021 i.widget.addClass('col-lg-5');
5022
5023 $('<div />')
5024 .addClass('col-lg-5')
5025 .text((typeof(this.options.description) == 'string') ? this.options.description : '')
5026 .appendTo(i.top);
5027 }
5028
5029 return i.top;
5030 },
5031
5032 active: function(sid)
5033 {
5034 return (this.instance[sid] && !this.instance[sid].disabled);
5035 },
5036
5037 ucipath: function(sid)
5038 {
5039 return {
5040 config: (this.options.uci_package || this.map.uci_package),
5041 section: (this.options.uci_section || sid),
5042 option: (this.options.uci_option || this.name)
5043 };
5044 },
5045
5046 ucivalue: function(sid)
5047 {
5048 var uci = this.ucipath(sid);
5049 var val = this.map.get(uci.config, uci.section, uci.option);
5050
5051 if (typeof(val) == 'undefined')
5052 return this.options.initial;
5053
5054 return val;
5055 },
5056
5057 formvalue: function(sid)
5058 {
5059 var v = $('#' + this.id(sid)).val();
5060 return (v === '') ? undefined : v;
5061 },
5062
5063 textvalue: function(sid)
5064 {
5065 var v = this.formvalue(sid);
5066
5067 if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
5068 v = this.ucivalue(sid);
5069
5070 if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
5071 v = this.options.placeholder;
5072
5073 if (typeof(v) == 'undefined' || v === '')
5074 return undefined;
5075
5076 if (typeof(v) == 'string' && $.isArray(this.choices))
5077 {
5078 for (var i = 0; i < this.choices.length; i++)
5079 if (v === this.choices[i][0])
5080 return this.choices[i][1];
5081 }
5082 else if (v === true)
5083 return L.tr('yes');
5084 else if (v === false)
5085 return L.tr('no');
5086 else if ($.isArray(v))
5087 return v.join(', ');
5088
5089 return v;
5090 },
5091
5092 changed: function(sid)
5093 {
5094 var a = this.ucivalue(sid);
5095 var b = this.formvalue(sid);
5096
5097 if (typeof(a) != typeof(b))
5098 return true;
5099
5100 if (typeof(a) == 'object')
5101 {
5102 if (a.length != b.length)
5103 return true;
5104
5105 for (var i = 0; i < a.length; i++)
5106 if (a[i] != b[i])
5107 return true;
5108
5109 return false;
5110 }
5111
5112 return (a != b);
5113 },
5114
5115 save: function(sid)
5116 {
5117 var uci = this.ucipath(sid);
5118
5119 if (this.instance[sid].disabled)
5120 {
5121 if (!this.options.keep)
5122 return this.map.set(uci.config, uci.section, uci.option, undefined);
5123
5124 return false;
5125 }
5126
5127 var chg = this.changed(sid);
5128 var val = this.formvalue(sid);
5129
5130 if (chg)
5131 this.map.set(uci.config, uci.section, uci.option, val);
5132
5133 return chg;
5134 },
5135
5136 _ev_validate: function(ev)
5137 {
5138 var d = ev.data;
5139 var rv = true;
5140 var val = d.elem.val();
5141 var vstack = d.vstack;
5142
5143 if (vstack && typeof(vstack[0]) == 'function')
5144 {
5145 delete validation.message;
5146
5147 if ((val.length == 0 && !d.opt))
5148 {
5149 d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
5150 d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
5151
5152 d.inst.error.text(L.tr('Field must not be empty')).show();
5153 rv = false;
5154 }
5155 else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
5156 {
5157 d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
5158 d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
5159
5160 d.inst.error.text(validation.message.format.apply(validation.message, vstack[1])).show();
5161 rv = false;
5162 }
5163 else
5164 {
5165 d.elem.parents('div.form-group, td').first().removeClass('luci2-form-error');
5166 d.elem.parents('div.input-group, div.form-group, td').first().removeClass('has-error');
5167
5168 if (d.multi && d.inst.widget && d.inst.widget.find('input.error, select.error').length > 0)
5169 rv = false;
5170 else
5171 d.inst.error.text('').hide();
5172 }
5173 }
5174
5175 if (rv)
5176 {
5177 for (var field in d.self.rdependency)
5178 d.self.rdependency[field].toggle(d.sid);
5179
5180 d.self.section.tabtoggle(d.sid);
5181 }
5182
5183 return rv;
5184 },
5185
5186 validator: function(sid, elem, multi)
5187 {
5188 var evdata = {
5189 self: this,
5190 sid: sid,
5191 elem: elem,
5192 multi: multi,
5193 inst: this.instance[sid],
5194 opt: this.options.optional
5195 };
5196
5197 if (this.events)
5198 for (var evname in this.events)
5199 elem.on(evname, evdata, this.events[evname]);
5200
5201 if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
5202 return elem;
5203
5204 var vstack;
5205 if (typeof(this.options.datatype) == 'string')
5206 {
5207 try {
5208 evdata.vstack = L.cbi.validation.compile(this.options.datatype);
5209 } catch(e) { };
5210 }
5211 else if (typeof(this.options.datatype) == 'function')
5212 {
5213 var vfunc = this.options.datatype;
5214 evdata.vstack = [ function(elem) {
5215 var rv = vfunc(this, elem);
5216 if (rv !== true)
5217 validation.message = rv;
5218 return (rv === true);
5219 }, [ elem ] ];
5220 }
5221
5222 if (elem.prop('tagName') == 'SELECT')
5223 {
5224 elem.change(evdata, this._ev_validate);
5225 }
5226 else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
5227 {
5228 elem.click(evdata, this._ev_validate);
5229 elem.blur(evdata, this._ev_validate);
5230 }
5231 else
5232 {
5233 elem.keyup(evdata, this._ev_validate);
5234 elem.blur(evdata, this._ev_validate);
5235 }
5236
5237 elem.attr('cbi-validate', true).on('validate', evdata, this._ev_validate);
5238
5239 return elem;
5240 },
5241
5242 validate: function(sid)
5243 {
5244 var i = this.instance[sid];
5245
5246 i.widget.find('[cbi-validate]').trigger('validate');
5247
5248 return (i.disabled || i.error.text() == '');
5249 },
5250
5251 depends: function(d, v, add)
5252 {
5253 var dep;
5254
5255 if ($.isArray(d))
5256 {
5257 dep = { };
5258 for (var i = 0; i < d.length; i++)
5259 {
5260 if (typeof(d[i]) == 'string')
5261 dep[d[i]] = true;
5262 else if (d[i] instanceof L.cbi.AbstractValue)
5263 dep[d[i].name] = true;
5264 }
5265 }
5266 else if (d instanceof L.cbi.AbstractValue)
5267 {
5268 dep = { };
5269 dep[d.name] = (typeof(v) == 'undefined') ? true : v;
5270 }
5271 else if (typeof(d) == 'object')
5272 {
5273 dep = d;
5274 }
5275 else if (typeof(d) == 'string')
5276 {
5277 dep = { };
5278 dep[d] = (typeof(v) == 'undefined') ? true : v;
5279 }
5280
5281 if (!dep || $.isEmptyObject(dep))
5282 return this;
5283
5284 for (var field in dep)
5285 {
5286 var f = this.section.fields[field];
5287 if (f)
5288 f.rdependency[this.name] = this;
5289 else
5290 delete dep[field];
5291 }
5292
5293 if ($.isEmptyObject(dep))
5294 return this;
5295
5296 if (!add || !this.dependencies.length)
5297 this.dependencies.push(dep);
5298 else
5299 for (var i = 0; i < this.dependencies.length; i++)
5300 $.extend(this.dependencies[i], dep);
5301
5302 return this;
5303 },
5304
5305 toggle: function(sid)
5306 {
5307 var d = this.dependencies;
5308 var i = this.instance[sid];
5309
5310 if (!d.length)
5311 return true;
5312
5313 for (var n = 0; n < d.length; n++)
5314 {
5315 var rv = true;
5316
5317 for (var field in d[n])
5318 {
5319 var val = this.section.fields[field].formvalue(sid);
5320 var cmp = d[n][field];
5321
5322 if (typeof(cmp) == 'boolean')
5323 {
5324 if (cmp == (typeof(val) == 'undefined' || val === '' || val === false))
5325 {
5326 rv = false;
5327 break;
5328 }
5329 }
5330 else if (typeof(cmp) == 'string' || typeof(cmp) == 'number')
5331 {
5332 if (val != cmp)
5333 {
5334 rv = false;
5335 break;
5336 }
5337 }
5338 else if (typeof(cmp) == 'function')
5339 {
5340 if (!cmp(val))
5341 {
5342 rv = false;
5343 break;
5344 }
5345 }
5346 else if (cmp instanceof RegExp)
5347 {
5348 if (!cmp.test(val))
5349 {
5350 rv = false;
5351 break;
5352 }
5353 }
5354 }
5355
5356 if (rv)
5357 {
5358 if (i.disabled)
5359 {
5360 i.disabled = false;
5361 i.top.fadeIn();
5362 }
5363
5364 return true;
5365 }
5366 }
5367
5368 if (!i.disabled)
5369 {
5370 i.disabled = true;
5371 i.top.is(':visible') ? i.top.fadeOut() : i.top.hide();
5372 }
5373
5374 return false;
5375 }
5376 });
5377
5378 this.cbi.CheckboxValue = this.cbi.AbstractValue.extend({
5379 widget: function(sid)
5380 {
5381 var o = this.options;
5382
5383 if (typeof(o.enabled) == 'undefined') o.enabled = '1';
5384 if (typeof(o.disabled) == 'undefined') o.disabled = '0';
5385
5386 var i = $('<input />')
5387 .attr('id', this.id(sid))
5388 .attr('type', 'checkbox')
5389 .prop('checked', this.ucivalue(sid));
5390
5391 return $('<div />')
5392 .addClass('checkbox')
5393 .append(this.validator(sid, i));
5394 },
5395
5396 ucivalue: function(sid)
5397 {
5398 var v = this.callSuper('ucivalue', sid);
5399
5400 if (typeof(v) == 'boolean')
5401 return v;
5402
5403 return (v == this.options.enabled);
5404 },
5405
5406 formvalue: function(sid)
5407 {
5408 var v = $('#' + this.id(sid)).prop('checked');
5409
5410 if (typeof(v) == 'undefined')
5411 return !!this.options.initial;
5412
5413 return v;
5414 },
5415
5416 save: function(sid)
5417 {
5418 var uci = this.ucipath(sid);
5419
5420 if (this.instance[sid].disabled)
5421 {
5422 if (!this.options.keep)
5423 return this.map.set(uci.config, uci.section, uci.option, undefined);
5424
5425 return false;
5426 }
5427
5428 var chg = this.changed(sid);
5429 var val = this.formvalue(sid);
5430
5431 if (chg)
5432 {
5433 if (this.options.optional && val == this.options.initial)
5434 this.map.set(uci.config, uci.section, uci.option, undefined);
5435 else
5436 this.map.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
5437 }
5438
5439 return chg;
5440 }
5441 });
5442
5443 this.cbi.InputValue = this.cbi.AbstractValue.extend({
5444 widget: function(sid)
5445 {
5446 var i = $('<input />')
5447 .addClass('form-control')
5448 .attr('id', this.id(sid))
5449 .attr('type', 'text')
5450 .attr('placeholder', this.options.placeholder)
5451 .val(this.ucivalue(sid));
5452
5453 return this.validator(sid, i);
5454 }
5455 });
5456
5457 this.cbi.PasswordValue = this.cbi.AbstractValue.extend({
5458 widget: function(sid)
5459 {
5460 var i = $('<input />')
5461 .addClass('form-control')
5462 .attr('id', this.id(sid))
5463 .attr('type', 'password')
5464 .attr('placeholder', this.options.placeholder)
5465 .val(this.ucivalue(sid));
5466
5467 var t = $('<span />')
5468 .addClass('input-group-btn')
5469 .append(L.ui.button(L.tr('Reveal'), 'default')
5470 .click(function(ev) {
5471 var b = $(this);
5472 var i = b.parent().prev();
5473 var t = i.attr('type');
5474 b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal'));
5475 i.attr('type', (t == 'password') ? 'text' : 'password');
5476 b = i = t = null;
5477 }));
5478
5479 this.validator(sid, i);
5480
5481 return $('<div />')
5482 .addClass('input-group')
5483 .append(i)
5484 .append(t);
5485 }
5486 });
5487
5488 this.cbi.ListValue = this.cbi.AbstractValue.extend({
5489 widget: function(sid)
5490 {
5491 var s = $('<select />')
5492 .addClass('form-control');
5493
5494 if (this.options.optional && !this.has_empty)
5495 $('<option />')
5496 .attr('value', '')
5497 .text(L.tr('-- Please choose --'))
5498 .appendTo(s);
5499
5500 if (this.choices)
5501 for (var i = 0; i < this.choices.length; i++)
5502 $('<option />')
5503 .attr('value', this.choices[i][0])
5504 .text(this.choices[i][1])
5505 .appendTo(s);
5506
5507 s.attr('id', this.id(sid)).val(this.ucivalue(sid));
5508
5509 return this.validator(sid, s);
5510 },
5511
5512 value: function(k, v)
5513 {
5514 if (!this.choices)
5515 this.choices = [ ];
5516
5517 if (k == '')
5518 this.has_empty = true;
5519
5520 this.choices.push([k, v || k]);
5521 return this;
5522 }
5523 });
5524
5525 this.cbi.MultiValue = this.cbi.ListValue.extend({
5526 widget: function(sid)
5527 {
5528 var v = this.ucivalue(sid);
5529 var t = $('<div />').attr('id', this.id(sid));
5530
5531 if (!$.isArray(v))
5532 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5533
5534 var s = { };
5535 for (var i = 0; i < v.length; i++)
5536 s[v[i]] = true;
5537
5538 if (this.choices)
5539 for (var i = 0; i < this.choices.length; i++)
5540 {
5541 $('<label />')
5542 .addClass('checkbox')
5543 .append($('<input />')
5544 .attr('type', 'checkbox')
5545 .attr('value', this.choices[i][0])
5546 .prop('checked', s[this.choices[i][0]]))
5547 .append(this.choices[i][1])
5548 .appendTo(t);
5549 }
5550
5551 return t;
5552 },
5553
5554 formvalue: function(sid)
5555 {
5556 var rv = [ ];
5557 var fields = $('#' + this.id(sid) + ' > label > input');
5558
5559 for (var i = 0; i < fields.length; i++)
5560 if (fields[i].checked)
5561 rv.push(fields[i].getAttribute('value'));
5562
5563 return rv;
5564 },
5565
5566 textvalue: function(sid)
5567 {
5568 var v = this.formvalue(sid);
5569 var c = { };
5570
5571 if (this.choices)
5572 for (var i = 0; i < this.choices.length; i++)
5573 c[this.choices[i][0]] = this.choices[i][1];
5574
5575 var t = [ ];
5576
5577 for (var i = 0; i < v.length; i++)
5578 t.push(c[v[i]] || v[i]);
5579
5580 return t.join(', ');
5581 }
5582 });
5583
5584 this.cbi.ComboBox = this.cbi.AbstractValue.extend({
5585 _change: function(ev)
5586 {
5587 var s = ev.target;
5588 var self = ev.data.self;
5589
5590 if (s.selectedIndex == (s.options.length - 1))
5591 {
5592 ev.data.select.hide();
5593 ev.data.input.show().focus();
5594 ev.data.input.val('');
5595 }
5596 else if (self.options.optional && s.selectedIndex == 0)
5597 {
5598 ev.data.input.val('');
5599 }
5600 else
5601 {
5602 ev.data.input.val(ev.data.select.val());
5603 }
5604
5605 ev.stopPropagation();
5606 },
5607
5608 _blur: function(ev)
5609 {
5610 var seen = false;
5611 var val = this.value;
5612 var self = ev.data.self;
5613
5614 ev.data.select.empty();
5615
5616 if (self.options.optional && !self.has_empty)
5617 $('<option />')
5618 .attr('value', '')
5619 .text(L.tr('-- please choose --'))
5620 .appendTo(ev.data.select);
5621
5622 if (self.choices)
5623 for (var i = 0; i < self.choices.length; i++)
5624 {
5625 if (self.choices[i][0] == val)
5626 seen = true;
5627
5628 $('<option />')
5629 .attr('value', self.choices[i][0])
5630 .text(self.choices[i][1])
5631 .appendTo(ev.data.select);
5632 }
5633
5634 if (!seen && val != '')
5635 $('<option />')
5636 .attr('value', val)
5637 .text(val)
5638 .appendTo(ev.data.select);
5639
5640 $('<option />')
5641 .attr('value', ' ')
5642 .text(L.tr('-- custom --'))
5643 .appendTo(ev.data.select);
5644
5645 ev.data.input.hide();
5646 ev.data.select.val(val).show().blur();
5647 },
5648
5649 _enter: function(ev)
5650 {
5651 if (ev.which != 13)
5652 return true;
5653
5654 ev.preventDefault();
5655 ev.data.self._blur(ev);
5656 return false;
5657 },
5658
5659 widget: function(sid)
5660 {
5661 var d = $('<div />')
5662 .attr('id', this.id(sid));
5663
5664 var t = $('<input />')
5665 .addClass('form-control')
5666 .attr('type', 'text')
5667 .hide()
5668 .appendTo(d);
5669
5670 var s = $('<select />')
5671 .addClass('form-control')
5672 .appendTo(d);
5673
5674 var evdata = {
5675 self: this,
5676 input: t,
5677 select: s
5678 };
5679
5680 s.change(evdata, this._change);
5681 t.blur(evdata, this._blur);
5682 t.keydown(evdata, this._enter);
5683
5684 t.val(this.ucivalue(sid));
5685 t.blur();
5686
5687 this.validator(sid, t);
5688 this.validator(sid, s);
5689
5690 return d;
5691 },
5692
5693 value: function(k, v)
5694 {
5695 if (!this.choices)
5696 this.choices = [ ];
5697
5698 if (k == '')
5699 this.has_empty = true;
5700
5701 this.choices.push([k, v || k]);
5702 return this;
5703 },
5704
5705 formvalue: function(sid)
5706 {
5707 var v = $('#' + this.id(sid)).children('input').val();
5708 return (v == '') ? undefined : v;
5709 }
5710 });
5711
5712 this.cbi.DynamicList = this.cbi.ComboBox.extend({
5713 _redraw: function(focus, add, del, s)
5714 {
5715 var v = s.values || [ ];
5716 delete s.values;
5717
5718 $(s.parent).children('div.input-group').children('input').each(function(i) {
5719 if (i != del)
5720 v.push(this.value || '');
5721 });
5722
5723 $(s.parent).empty();
5724
5725 if (add >= 0)
5726 {
5727 focus = add + 1;
5728 v.splice(focus, 0, '');
5729 }
5730 else if (v.length == 0)
5731 {
5732 focus = 0;
5733 v.push('');
5734 }
5735
5736 for (var i = 0; i < v.length; i++)
5737 {
5738 var evdata = {
5739 sid: s.sid,
5740 self: s.self,
5741 parent: s.parent,
5742 index: i,
5743 remove: ((i+1) < v.length)
5744 };
5745
5746 var btn;
5747 if (evdata.remove)
5748 btn = L.ui.button('–', 'danger').click(evdata, this._btnclick);
5749 else
5750 btn = L.ui.button('+', 'success').click(evdata, this._btnclick);
5751
5752 if (this.choices)
5753 {
5754 var txt = $('<input />')
5755 .addClass('form-control')
5756 .attr('type', 'text')
5757 .hide();
5758
5759 var sel = $('<select />')
5760 .addClass('form-control');
5761
5762 $('<div />')
5763 .addClass('input-group')
5764 .append(txt)
5765 .append(sel)
5766 .append($('<span />')
5767 .addClass('input-group-btn')
5768 .append(btn))
5769 .appendTo(s.parent);
5770
5771 evdata.input = this.validator(s.sid, txt, true);
5772 evdata.select = this.validator(s.sid, sel, true);
5773
5774 sel.change(evdata, this._change);
5775 txt.blur(evdata, this._blur);
5776 txt.keydown(evdata, this._keydown);
5777
5778 txt.val(v[i]);
5779 txt.blur();
5780
5781 if (i == focus || -(i+1) == focus)
5782 sel.focus();
5783
5784 sel = txt = null;
5785 }
5786 else
5787 {
5788 var f = $('<input />')
5789 .attr('type', 'text')
5790 .attr('index', i)
5791 .attr('placeholder', (i == 0) ? this.options.placeholder : '')
5792 .addClass('form-control')
5793 .keydown(evdata, this._keydown)
5794 .keypress(evdata, this._keypress)
5795 .val(v[i]);
5796
5797 $('<div />')
5798 .addClass('input-group')
5799 .append(f)
5800 .append($('<span />')
5801 .addClass('input-group-btn')
5802 .append(btn))
5803 .appendTo(s.parent);
5804
5805 if (i == focus)
5806 {
5807 f.focus();
5808 }
5809 else if (-(i+1) == focus)
5810 {
5811 f.focus();
5812
5813 /* force cursor to end */
5814 var val = f.val();
5815 f.val(' ');
5816 f.val(val);
5817 }
5818
5819 evdata.input = this.validator(s.sid, f, true);
5820
5821 f = null;
5822 }
5823
5824 evdata = null;
5825 }
5826
5827 s = null;
5828 },
5829
5830 _keypress: function(ev)
5831 {
5832 switch (ev.which)
5833 {
5834 /* backspace, delete */
5835 case 8:
5836 case 46:
5837 if (ev.data.input.val() == '')
5838 {
5839 ev.preventDefault();
5840 return false;
5841 }
5842
5843 return true;
5844
5845 /* enter, arrow up, arrow down */
5846 case 13:
5847 case 38:
5848 case 40:
5849 ev.preventDefault();
5850 return false;
5851 }
5852
5853 return true;
5854 },
5855
5856 _keydown: function(ev)
5857 {
5858 var input = ev.data.input;
5859
5860 switch (ev.which)
5861 {
5862 /* backspace, delete */
5863 case 8:
5864 case 46:
5865 if (input.val().length == 0)
5866 {
5867 ev.preventDefault();
5868
5869 var index = ev.data.index;
5870 var focus = index;
5871
5872 if (ev.which == 8)
5873 focus = -focus;
5874
5875 ev.data.self._redraw(focus, -1, index, ev.data);
5876 return false;
5877 }
5878
5879 break;
5880
5881 /* enter */
5882 case 13:
5883 ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
5884 break;
5885
5886 /* arrow up */
5887 case 38:
5888 var prev = input.parent().prevAll('div.input-group:first').children('input');
5889 if (prev.is(':visible'))
5890 prev.focus();
5891 else
5892 prev.next('select').focus();
5893 break;
5894
5895 /* arrow down */
5896 case 40:
5897 var next = input.parent().nextAll('div.input-group:first').children('input');
5898 if (next.is(':visible'))
5899 next.focus();
5900 else
5901 next.next('select').focus();
5902 break;
5903 }
5904
5905 return true;
5906 },
5907
5908 _btnclick: function(ev)
5909 {
5910 if (!this.getAttribute('disabled'))
5911 {
5912 if (ev.data.remove)
5913 {
5914 var index = ev.data.index;
5915 ev.data.self._redraw(-index, -1, index, ev.data);
5916 }
5917 else
5918 {
5919 ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
5920 }
5921 }
5922
5923 return false;
5924 },
5925
5926 widget: function(sid)
5927 {
5928 this.options.optional = true;
5929
5930 var v = this.ucivalue(sid);
5931
5932 if (!$.isArray(v))
5933 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5934
5935 var d = $('<div />')
5936 .attr('id', this.id(sid))
5937 .addClass('cbi-input-dynlist');
5938
5939 this._redraw(NaN, -1, -1, {
5940 self: this,
5941 parent: d[0],
5942 values: v,
5943 sid: sid
5944 });
5945
5946 return d;
5947 },
5948
5949 ucivalue: function(sid)
5950 {
5951 var v = this.callSuper('ucivalue', sid);
5952
5953 if (!$.isArray(v))
5954 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5955
5956 return v;
5957 },
5958
5959 formvalue: function(sid)
5960 {
5961 var rv = [ ];
5962 var fields = $('#' + this.id(sid) + ' input');
5963
5964 for (var i = 0; i < fields.length; i++)
5965 if (typeof(fields[i].value) == 'string' && fields[i].value.length)
5966 rv.push(fields[i].value);
5967
5968 return rv;
5969 }
5970 });
5971
5972 this.cbi.DummyValue = this.cbi.AbstractValue.extend({
5973 widget: function(sid)
5974 {
5975 return $('<div />')
5976 .addClass('form-control-static')
5977 .attr('id', this.id(sid))
5978 .html(this.ucivalue(sid));
5979 },
5980
5981 formvalue: function(sid)
5982 {
5983 return this.ucivalue(sid);
5984 }
5985 });
5986
5987 this.cbi.ButtonValue = this.cbi.AbstractValue.extend({
5988 widget: function(sid)
5989 {
5990 this.options.optional = true;
5991
5992 var btn = $('<button />')
5993 .addClass('btn btn-default')
5994 .attr('id', this.id(sid))
5995 .attr('type', 'button')
5996 .text(this.label('text'));
5997
5998 return this.validator(sid, btn);
5999 }
6000 });
6001
6002 this.cbi.NetworkList = this.cbi.AbstractValue.extend({
6003 load: function(sid)
6004 {
6005 return L.NetworkModel.init();
6006 },
6007
6008 _device_icon: function(dev)
6009 {
6010 return $('<img />')
6011 .attr('src', dev.icon())
6012 .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'));
6013 },
6014
6015 widget: function(sid)
6016 {
6017 var id = this.id(sid);
6018 var ul = $('<ul />')
6019 .attr('id', id)
6020 .addClass('list-unstyled');
6021
6022 var itype = this.options.multiple ? 'checkbox' : 'radio';
6023 var value = this.ucivalue(sid);
6024 var check = { };
6025
6026 if (!this.options.multiple)
6027 check[value] = true;
6028 else
6029 for (var i = 0; i < value.length; i++)
6030 check[value[i]] = true;
6031
6032 var interfaces = L.NetworkModel.getInterfaces();
6033
6034 for (var i = 0; i < interfaces.length; i++)
6035 {
6036 var iface = interfaces[i];
6037
6038 $('<li />')
6039 .append($('<label />')
6040 .addClass(itype + ' inline')
6041 .append(this.validator(sid, $('<input />')
6042 .attr('name', itype + id)
6043 .attr('type', itype)
6044 .attr('value', iface.name())
6045 .prop('checked', !!check[iface.name()]), true))
6046 .append(iface.renderBadge()))
6047 .appendTo(ul);
6048 }
6049
6050 if (!this.options.multiple)
6051 {
6052 $('<li />')
6053 .append($('<label />')
6054 .addClass(itype + ' inline text-muted')
6055 .append($('<input />')
6056 .attr('name', itype + id)
6057 .attr('type', itype)
6058 .attr('value', '')
6059 .prop('checked', $.isEmptyObject(check)))
6060 .append(L.tr('unspecified')))
6061 .appendTo(ul);
6062 }
6063
6064 return ul;
6065 },
6066
6067 ucivalue: function(sid)
6068 {
6069 var v = this.callSuper('ucivalue', sid);
6070
6071 if (!this.options.multiple)
6072 {
6073 if ($.isArray(v))
6074 {
6075 return v[0];
6076 }
6077 else if (typeof(v) == 'string')
6078 {
6079 v = v.match(/\S+/);
6080 return v ? v[0] : undefined;
6081 }
6082
6083 return v;
6084 }
6085 else
6086 {
6087 if (typeof(v) == 'string')
6088 v = v.match(/\S+/g);
6089
6090 return v || [ ];
6091 }
6092 },
6093
6094 formvalue: function(sid)
6095 {
6096 var inputs = $('#' + this.id(sid) + ' input');
6097
6098 if (!this.options.multiple)
6099 {
6100 for (var i = 0; i < inputs.length; i++)
6101 if (inputs[i].checked && inputs[i].value !== '')
6102 return inputs[i].value;
6103
6104 return undefined;
6105 }
6106
6107 var rv = [ ];
6108
6109 for (var i = 0; i < inputs.length; i++)
6110 if (inputs[i].checked)
6111 rv.push(inputs[i].value);
6112
6113 return rv.length ? rv : undefined;
6114 }
6115 });
6116
6117 this.cbi.DeviceList = this.cbi.NetworkList.extend({
6118 _ev_focus: function(ev)
6119 {
6120 var self = ev.data.self;
6121 var input = $(this);
6122
6123 input.parent().prev().prop('checked', true);
6124 },
6125
6126 _ev_blur: function(ev)
6127 {
6128 ev.which = 10;
6129 ev.data.self._ev_keydown.call(this, ev);
6130 },
6131
6132 _ev_keydown: function(ev)
6133 {
6134 if (ev.which != 10 && ev.which != 13)
6135 return;
6136
6137 var sid = ev.data.sid;
6138 var self = ev.data.self;
6139 var input = $(this);
6140 var ifnames = L.toArray(input.val());
6141
6142 if (!ifnames.length)
6143 return;
6144
6145 L.NetworkModel.createDevice(ifnames[0]);
6146
6147 self._redraw(sid, $('#' + self.id(sid)), ifnames[0]);
6148 },
6149
6150 load: function(sid)
6151 {
6152 return L.NetworkModel.init();
6153 },
6154
6155 _redraw: function(sid, ul, sel)
6156 {
6157 var id = ul.attr('id');
6158 var devs = L.NetworkModel.getDevices();
6159 var iface = L.NetworkModel.getInterface(sid);
6160 var itype = this.options.multiple ? 'checkbox' : 'radio';
6161 var check = { };
6162
6163 if (!sel)
6164 {
6165 for (var i = 0; i < devs.length; i++)
6166 if (devs[i].isInNetwork(iface))
6167 check[devs[i].name()] = true;
6168 }
6169 else
6170 {
6171 if (this.options.multiple)
6172 check = L.toObject(this.formvalue(sid));
6173
6174 check[sel] = true;
6175 }
6176
6177 ul.empty();
6178
6179 for (var i = 0; i < devs.length; i++)
6180 {
6181 var dev = devs[i];
6182
6183 if (dev.isBridge() && this.options.bridges === false)
6184 continue;
6185
6186 if (!dev.isBridgeable() && this.options.multiple)
6187 continue;
6188
6189 var badge = $('<span />')
6190 .addClass('badge')
6191 .append($('<img />').attr('src', dev.icon()))
6192 .append(' %s: %s'.format(dev.name(), dev.description()));
6193
6194 //var ifcs = dev.getInterfaces();
6195 //if (ifcs.length)
6196 //{
6197 // for (var j = 0; j < ifcs.length; j++)
6198 // badge.append((j ? ', ' : ' (') + ifcs[j].name());
6199 //
6200 // badge.append(')');
6201 //}
6202
6203 $('<li />')
6204 .append($('<label />')
6205 .addClass(itype + ' inline')
6206 .append($('<input />')
6207 .attr('name', itype + id)
6208 .attr('type', itype)
6209 .attr('value', dev.name())
6210 .prop('checked', !!check[dev.name()]))
6211 .append(badge))
6212 .appendTo(ul);
6213 }
6214
6215
6216 $('<li />')
6217 .append($('<label />')
6218 .attr('for', 'custom' + id)
6219 .addClass(itype + ' inline')
6220 .append($('<input />')
6221 .attr('name', itype + id)
6222 .attr('type', itype)
6223 .attr('value', ''))
6224 .append($('<span />')
6225 .addClass('badge')
6226 .append($('<input />')
6227 .attr('id', 'custom' + id)
6228 .attr('type', 'text')
6229 .attr('placeholder', L.tr('Custom device …'))
6230 .on('focus', { self: this, sid: sid }, this._ev_focus)
6231 .on('blur', { self: this, sid: sid }, this._ev_blur)
6232 .on('keydown', { self: this, sid: sid }, this._ev_keydown))))
6233 .appendTo(ul);
6234
6235 if (!this.options.multiple)
6236 {
6237 $('<li />')
6238 .append($('<label />')
6239 .addClass(itype + ' inline text-muted')
6240 .append($('<input />')
6241 .attr('name', itype + id)
6242 .attr('type', itype)
6243 .attr('value', '')
6244 .prop('checked', $.isEmptyObject(check)))
6245 .append(L.tr('unspecified')))
6246 .appendTo(ul);
6247 }
6248 },
6249
6250 widget: function(sid)
6251 {
6252 var id = this.id(sid);
6253 var ul = $('<ul />')
6254 .attr('id', id)
6255 .addClass('list-unstyled');
6256
6257 this._redraw(sid, ul);
6258
6259 return ul;
6260 },
6261
6262 save: function(sid)
6263 {
6264 if (this.instance[sid].disabled)
6265 return;
6266
6267 var ifnames = this.formvalue(sid);
6268 //if (!ifnames)
6269 // return;
6270
6271 var iface = L.NetworkModel.getInterface(sid);
6272 if (!iface)
6273 return;
6274
6275 iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]);
6276 }
6277 });
6278
6279
6280 this.cbi.AbstractSection = this.ui.AbstractWidget.extend({
6281 id: function()
6282 {
6283 var s = [ arguments[0], this.map.uci_package, this.uci_type ];
6284
6285 for (var i = 1; i < arguments.length; i++)
6286 s.push(arguments[i].replace(/\./g, '_'));
6287
6288 return s.join('_');
6289 },
6290
6291 option: function(widget, name, options)
6292 {
6293 if (this.tabs.length == 0)
6294 this.tab({ id: '__default__', selected: true });
6295
6296 return this.taboption('__default__', widget, name, options);
6297 },
6298
6299 tab: function(options)
6300 {
6301 if (options.selected)
6302 this.tabs.selected = this.tabs.length;
6303
6304 this.tabs.push({
6305 id: options.id,
6306 caption: options.caption,
6307 description: options.description,
6308 fields: [ ],
6309 li: { }
6310 });
6311 },
6312
6313 taboption: function(tabid, widget, name, options)
6314 {
6315 var tab;
6316 for (var i = 0; i < this.tabs.length; i++)
6317 {
6318 if (this.tabs[i].id == tabid)
6319 {
6320 tab = this.tabs[i];
6321 break;
6322 }
6323 }
6324
6325 if (!tab)
6326 throw 'Cannot append to unknown tab ' + tabid;
6327
6328 var w = widget ? new widget(name, options) : null;
6329
6330 if (!(w instanceof L.cbi.AbstractValue))
6331 throw 'Widget must be an instance of AbstractValue';
6332
6333 w.section = this;
6334 w.map = this.map;
6335
6336 this.fields[name] = w;
6337 tab.fields.push(w);
6338
6339 return w;
6340 },
6341
6342 tabtoggle: function(sid)
6343 {
6344 for (var i = 0; i < this.tabs.length; i++)
6345 {
6346 var tab = this.tabs[i];
6347 var elem = $('#' + this.id('nodetab', sid, tab.id));
6348 var empty = true;
6349
6350 for (var j = 0; j < tab.fields.length; j++)
6351 {
6352 if (tab.fields[j].active(sid))
6353 {
6354 empty = false;
6355 break;
6356 }
6357 }
6358
6359 if (empty && elem.is(':visible'))
6360 elem.fadeOut();
6361 else if (!empty)
6362 elem.fadeIn();
6363 }
6364 },
6365
6366 ucipackages: function(pkg)
6367 {
6368 for (var i = 0; i < this.tabs.length; i++)
6369 for (var j = 0; j < this.tabs[i].fields.length; j++)
6370 if (this.tabs[i].fields[j].options.uci_package)
6371 pkg[this.tabs[i].fields[j].options.uci_package] = true;
6372 },
6373
6374 formvalue: function()
6375 {
6376 var rv = { };
6377
6378 this.sections(function(s) {
6379 var sid = s['.name'];
6380 var sv = rv[sid] || (rv[sid] = { });
6381
6382 for (var i = 0; i < this.tabs.length; i++)
6383 for (var j = 0; j < this.tabs[i].fields.length; j++)
6384 {
6385 var val = this.tabs[i].fields[j].formvalue(sid);
6386 sv[this.tabs[i].fields[j].name] = val;
6387 }
6388 });
6389
6390 return rv;
6391 },
6392
6393 validate_section: function(sid)
6394 {
6395 var inst = this.instance[sid];
6396
6397 var invals = 0;
6398 var badge = $('#' + this.id('teaser', sid)).children('span:first');
6399
6400 for (var i = 0; i < this.tabs.length; i++)
6401 {
6402 var inval = 0;
6403 var stbadge = $('#' + this.id('nodetab', sid, this.tabs[i].id)).children('span:first');
6404
6405 for (var j = 0; j < this.tabs[i].fields.length; j++)
6406 if (!this.tabs[i].fields[j].validate(sid))
6407 inval++;
6408
6409 if (inval > 0)
6410 stbadge.show()
6411 .text(inval)
6412 .attr('title', L.trp('1 Error', '%d Errors', inval).format(inval));
6413 else
6414 stbadge.hide();
6415
6416 invals += inval;
6417 }
6418
6419 if (invals > 0)
6420 badge.show()
6421 .text(invals)
6422 .attr('title', L.trp('1 Error', '%d Errors', invals).format(invals));
6423 else
6424 badge.hide();
6425
6426 return invals;
6427 },
6428
6429 validate: function()
6430 {
6431 var errors = 0;
6432 var as = this.sections();
6433
6434 for (var i = 0; i < as.length; i++)
6435 {
6436 var invals = this.validate_section(as[i]['.name']);
6437
6438 if (invals > 0)
6439 errors += invals;
6440 }
6441
6442 var badge = $('#' + this.id('sectiontab')).children('span:first');
6443
6444 if (errors > 0)
6445 badge.show()
6446 .text(errors)
6447 .attr('title', L.trp('1 Error', '%d Errors', errors).format(errors));
6448 else
6449 badge.hide();
6450
6451 return (errors == 0);
6452 }
6453 });
6454
6455 this.cbi.TypedSection = this.cbi.AbstractSection.extend({
6456 init: function(uci_type, options)
6457 {
6458 this.uci_type = uci_type;
6459 this.options = options;
6460 this.tabs = [ ];
6461 this.fields = { };
6462 this.active_panel = 0;
6463 this.active_tab = { };
6464 },
6465
6466 filter: function(section)
6467 {
6468 return true;
6469 },
6470
6471 sections: function(cb)
6472 {
6473 var s1 = L.uci.sections(this.map.uci_package);
6474 var s2 = [ ];
6475
6476 for (var i = 0; i < s1.length; i++)
6477 if (s1[i]['.type'] == this.uci_type)
6478 if (this.filter(s1[i]))
6479 s2.push(s1[i]);
6480
6481 if (typeof(cb) == 'function')
6482 for (var i = 0; i < s2.length; i++)
6483 cb.call(this, s2[i]);
6484
6485 return s2;
6486 },
6487
6488 add: function(name)
6489 {
6490 return this.map.add(this.map.uci_package, this.uci_type, name);
6491 },
6492
6493 remove: function(sid)
6494 {
6495 return this.map.remove(this.map.uci_package, sid);
6496 },
6497
6498 _ev_add: function(ev)
6499 {
6500 var addb = $(this);
6501 var name = undefined;
6502 var self = ev.data.self;
6503
6504 if (addb.prev().prop('nodeName') == 'INPUT')
6505 name = addb.prev().val();
6506
6507 if (addb.prop('disabled') || name === '')
6508 return;
6509
6510 L.ui.saveScrollTop();
6511
6512 self.active_panel = -1;
6513 self.map.save();
6514
6515 ev.data.sid = self.add(name);
6516 ev.data.type = self.uci_type;
6517 ev.data.name = name;
6518
6519 self.trigger('add', ev);
6520
6521 self.map.redraw();
6522
6523 L.ui.restoreScrollTop();
6524 },
6525
6526 _ev_remove: function(ev)
6527 {
6528 var self = ev.data.self;
6529 var sid = ev.data.sid;
6530
6531 L.ui.saveScrollTop();
6532
6533 self.trigger('remove', ev);
6534
6535 self.map.save();
6536 self.remove(sid);
6537 self.map.redraw();
6538
6539 L.ui.restoreScrollTop();
6540
6541 ev.stopPropagation();
6542 },
6543
6544 _ev_sid: function(ev)
6545 {
6546 var self = ev.data.self;
6547 var text = $(this);
6548 var addb = text.next();
6549 var errt = addb.next();
6550 var name = text.val();
6551
6552 if (!/^[a-zA-Z0-9_]*$/.test(name))
6553 {
6554 errt.text(L.tr('Invalid section name')).show();
6555 text.addClass('error');
6556 addb.prop('disabled', true);
6557 return false;
6558 }
6559
6560 if (L.uci.get(self.map.uci_package, name))
6561 {
6562 errt.text(L.tr('Name already used')).show();
6563 text.addClass('error');
6564 addb.prop('disabled', true);
6565 return false;
6566 }
6567
6568 errt.text('').hide();
6569 text.removeClass('error');
6570 addb.prop('disabled', false);
6571 return true;
6572 },
6573
6574 _ev_tab: function(ev)
6575 {
6576 var self = ev.data.self;
6577 var sid = ev.data.sid;
6578
6579 self.validate();
6580 self.active_tab[sid] = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
6581 },
6582
6583 _ev_panel_collapse: function(ev)
6584 {
6585 var self = ev.data.self;
6586
6587 var this_panel = $(ev.target);
6588 var this_toggle = this_panel.prevAll('[data-toggle="collapse"]:first');
6589
6590 var prev_toggle = $($(ev.delegateTarget).find('[data-toggle="collapse"]:eq(%d)'.format(self.active_panel)));
6591 var prev_panel = $(prev_toggle.attr('data-target'));
6592
6593 prev_panel
6594 .removeClass('in')
6595 .addClass('collapse');
6596
6597 prev_toggle.find('.luci2-section-teaser')
6598 .show()
6599 .children('span:last')
6600 .empty()
6601 .append(self.teaser(prev_panel.attr('data-luci2-sid')));
6602
6603 this_toggle.find('.luci2-section-teaser')
6604 .hide();
6605
6606 self.active_panel = parseInt(this_panel.attr('data-luci2-panel-index'));
6607 self.validate();
6608 },
6609
6610 _ev_panel_open: function(ev)
6611 {
6612 var self = ev.data.self;
6613 var panel = $($(this).attr('data-target'));
6614 var index = parseInt(panel.attr('data-luci2-panel-index'));
6615
6616 if (index == self.active_panel)
6617 ev.stopPropagation();
6618 },
6619
6620 _ev_sort: function(ev)
6621 {
6622 var self = ev.data.self;
6623 var cur_idx = ev.data.index;
6624 var new_idx = cur_idx + (ev.data.up ? -1 : 1);
6625 var s = self.sections();
6626
6627 if (new_idx >= 0 && new_idx < s.length)
6628 {
6629 L.uci.swap(self.map.uci_package, s[cur_idx]['.name'], s[new_idx]['.name']);
6630
6631 self.map.save();
6632 self.map.redraw();
6633 }
6634
6635 ev.stopPropagation();
6636 },
6637
6638 teaser: function(sid)
6639 {
6640 var tf = this.teaser_fields;
6641
6642 if (!tf)
6643 {
6644 tf = this.teaser_fields = [ ];
6645
6646 if ($.isArray(this.options.teasers))
6647 {
6648 for (var i = 0; i < this.options.teasers.length; i++)
6649 {
6650 var f = this.options.teasers[i];
6651 if (f instanceof L.cbi.AbstractValue)
6652 tf.push(f);
6653 else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue)
6654 tf.push(this.fields[f]);
6655 }
6656 }
6657 else
6658 {
6659 for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
6660 for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
6661 tf.push(this.tabs[i].fields[j]);
6662 }
6663 }
6664
6665 var t = '';
6666
6667 for (var i = 0; i < tf.length; i++)
6668 {
6669 if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
6670 continue;
6671
6672 var n = tf[i].options.caption || tf[i].name;
6673 var v = tf[i].textvalue(sid);
6674
6675 if (typeof(v) == 'undefined')
6676 continue;
6677
6678 t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
6679 }
6680
6681 return t;
6682 },
6683
6684 _render_add: function()
6685 {
6686 if (!this.options.addremove)
6687 return null;
6688
6689 var text = L.tr('Add section');
6690 var ttip = L.tr('Create new section...');
6691
6692 if ($.isArray(this.options.add_caption))
6693 text = this.options.add_caption[0], ttip = this.options.add_caption[1];
6694 else if (typeof(this.options.add_caption) == 'string')
6695 text = this.options.add_caption, ttip = '';
6696
6697 var add = $('<div />');
6698
6699 if (this.options.anonymous === false)
6700 {
6701 $('<input />')
6702 .addClass('cbi-input-text')
6703 .attr('type', 'text')
6704 .attr('placeholder', ttip)
6705 .blur({ self: this }, this._ev_sid)
6706 .keyup({ self: this }, this._ev_sid)
6707 .appendTo(add);
6708
6709 $('<img />')
6710 .attr('src', L.globals.resource + '/icons/cbi/add.gif')
6711 .attr('title', text)
6712 .addClass('cbi-button')
6713 .click({ self: this }, this._ev_add)
6714 .appendTo(add);
6715
6716 $('<div />')
6717 .addClass('cbi-value-error')
6718 .hide()
6719 .appendTo(add);
6720 }
6721 else
6722 {
6723 L.ui.button(text, 'success', ttip)
6724 .click({ self: this }, this._ev_add)
6725 .appendTo(add);
6726 }
6727
6728 return add;
6729 },
6730
6731 _render_remove: function(sid, index)
6732 {
6733 if (!this.options.addremove)
6734 return null;
6735
6736 var text = L.tr('Remove');
6737 var ttip = L.tr('Remove this section');
6738
6739 if ($.isArray(this.options.remove_caption))
6740 text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
6741 else if (typeof(this.options.remove_caption) == 'string')
6742 text = this.options.remove_caption, ttip = '';
6743
6744 return L.ui.button(text, 'danger', ttip)
6745 .click({ self: this, sid: sid, index: index }, this._ev_remove);
6746 },
6747
6748 _render_sort: function(sid, index)
6749 {
6750 if (!this.options.sortable)
6751 return null;
6752
6753 var b1 = L.ui.button('↑', 'info', L.tr('Move up'))
6754 .click({ self: this, index: index, up: true }, this._ev_sort);
6755
6756 var b2 = L.ui.button('↓', 'info', L.tr('Move down'))
6757 .click({ self: this, index: index, up: false }, this._ev_sort);
6758
6759 return b1.add(b2);
6760 },
6761
6762 _render_caption: function()
6763 {
6764 return $('<h3 />')
6765 .addClass('panel-title')
6766 .append(this.label('caption') || this.uci_type);
6767 },
6768
6769 _render_description: function()
6770 {
6771 var text = this.label('description');
6772
6773 if (text)
6774 return $('<div />')
6775 .addClass('luci2-section-description')
6776 .text(text);
6777
6778 return null;
6779 },
6780
6781 _render_teaser: function(sid, index)
6782 {
6783 if (this.options.collabsible || this.map.options.collabsible)
6784 {
6785 return $('<div />')
6786 .attr('id', this.id('teaser', sid))
6787 .addClass('luci2-section-teaser well well-sm')
6788 .append($('<span />')
6789 .addClass('badge'))
6790 .append($('<span />'));
6791 }
6792
6793 return null;
6794 },
6795
6796 _render_head: function(condensed)
6797 {
6798 if (condensed)
6799 return null;
6800
6801 return $('<div />')
6802 .addClass('panel-heading')
6803 .append(this._render_caption())
6804 .append(this._render_description());
6805 },
6806
6807 _render_tab_description: function(sid, index, tab_index)
6808 {
6809 var tab = this.tabs[tab_index];
6810
6811 if (typeof(tab.description) == 'string')
6812 {
6813 return $('<div />')
6814 .addClass('cbi-tab-descr')
6815 .text(tab.description);
6816 }
6817
6818 return null;
6819 },
6820
6821 _render_tab_head: function(sid, index, tab_index)
6822 {
6823 var tab = this.tabs[tab_index];
6824 var cur = this.active_tab[sid] || 0;
6825
6826 var tabh = $('<li />')
6827 .append($('<a />')
6828 .attr('id', this.id('nodetab', sid, tab.id))
6829 .attr('href', '#' + this.id('node', sid, tab.id))
6830 .attr('data-toggle', 'tab')
6831 .attr('data-luci2-tab-index', tab_index)
6832 .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ')
6833 .append($('<span />')
6834 .addClass('badge'))
6835 .on('shown.bs.tab', { self: this, sid: sid }, this._ev_tab));
6836
6837 if (cur == tab_index)
6838 tabh.addClass('active');
6839
6840 if (!tab.fields.length)
6841 tabh.hide();
6842
6843 return tabh;
6844 },
6845
6846 _render_tab_body: function(sid, index, tab_index)
6847 {
6848 var tab = this.tabs[tab_index];
6849 var cur = this.active_tab[sid] || 0;
6850
6851 var tabb = $('<div />')
6852 .addClass('tab-pane')
6853 .attr('id', this.id('node', sid, tab.id))
6854 .attr('data-luci2-tab-index', tab_index)
6855 .append(this._render_tab_description(sid, index, tab_index));
6856
6857 if (cur == tab_index)
6858 tabb.addClass('active');
6859
6860 for (var i = 0; i < tab.fields.length; i++)
6861 tabb.append(tab.fields[i].render(sid));
6862
6863 return tabb;
6864 },
6865
6866 _render_section_head: function(sid, index)
6867 {
6868 var head = $('<div />')
6869 .addClass('luci2-section-header')
6870 .append(this._render_teaser(sid, index))
6871 .append($('<div />')
6872 .addClass('btn-group')
6873 .append(this._render_sort(sid, index))
6874 .append(this._render_remove(sid, index)));
6875
6876 if (this.options.collabsible)
6877 {
6878 head.attr('data-toggle', 'collapse')
6879 .attr('data-parent', this.id('sectiongroup'))
6880 .attr('data-target', '#' + this.id('panel', sid))
6881 .on('click', { self: this }, this._ev_panel_open);
6882 }
6883
6884 return head;
6885 },
6886
6887 _render_section_body: function(sid, index)
6888 {
6889 var body = $('<div />')
6890 .attr('id', this.id('panel', sid))
6891 .attr('data-luci2-panel-index', index)
6892 .attr('data-luci2-sid', sid);
6893
6894 if (this.options.collabsible || this.map.options.collabsible)
6895 {
6896 body.addClass('panel-collapse collapse');
6897
6898 if (index == this.active_panel)
6899 body.addClass('in');
6900 }
6901
6902 var tab_heads = $('<ul />')
6903 .addClass('nav nav-tabs');
6904
6905 var tab_bodies = $('<div />')
6906 .addClass('form-horizontal tab-content')
6907 .append(tab_heads);
6908
6909 for (var j = 0; j < this.tabs.length; j++)
6910 {
6911 tab_heads.append(this._render_tab_head(sid, index, j));
6912 tab_bodies.append(this._render_tab_body(sid, index, j));
6913 }
6914
6915 body.append(tab_bodies);
6916
6917 if (this.tabs.length <= 1)
6918 tab_heads.hide();
6919
6920 return body;
6921 },
6922
6923 _render_body: function(condensed)
6924 {
6925 var s = this.sections();
6926
6927 if (this.active_panel < 0)
6928 this.active_panel += s.length;
6929 else if (this.active_panel >= s.length)
6930 this.active_panel = s.length - 1;
6931
6932 var body = $('<ul />')
6933 .addClass('list-group');
6934
6935 if (this.options.collabsible)
6936 {
6937 body.attr('id', this.id('sectiongroup'))
6938 .on('show.bs.collapse', { self: this }, this._ev_panel_collapse);
6939 }
6940
6941 if (s.length == 0)
6942 {
6943 body.append($('<li />')
6944 .addClass('list-group-item text-muted')
6945 .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))
6946 }
6947
6948 for (var i = 0; i < s.length; i++)
6949 {
6950 var sid = s[i]['.name'];
6951 var inst = this.instance[sid] = { tabs: [ ] };
6952
6953 body.append($('<li />')
6954 .addClass('list-group-item')
6955 .append(this._render_section_head(sid, i))
6956 .append(this._render_section_body(sid, i)));
6957 }
6958
6959 return body;
6960 },
6961
6962 render: function(condensed)
6963 {
6964 this.instance = { };
6965
6966 var panel = $('<div />')
6967 .addClass('panel panel-default')
6968 .append(this._render_head(condensed))
6969 .append(this._render_body(condensed));
6970
6971 if (this.options.addremove)
6972 panel.append($('<div />')
6973 .addClass('panel-footer')
6974 .append(this._render_add()));
6975
6976 return panel;
6977 },
6978
6979 finish: function()
6980 {
6981 var s = this.sections();
6982
6983 for (var i = 0; i < s.length; i++)
6984 {
6985 var sid = s[i]['.name'];
6986
6987 this.validate_section(sid);
6988
6989 if (i != this.active_panel)
6990 $('#' + this.id('teaser', sid)).children('span:last')
6991 .append(this.teaser(sid));
6992 else
6993 $('#' + this.id('teaser', sid))
6994 .hide();
6995 }
6996 }
6997 });
6998
6999 this.cbi.TableSection = this.cbi.TypedSection.extend({
7000 _render_table_head: function()
7001 {
7002 var thead = $('<thead />')
7003 .append($('<tr />')
7004 .addClass('cbi-section-table-titles'));
7005
7006 for (var j = 0; j < this.tabs[0].fields.length; j++)
7007 thead.children().append($('<th />')
7008 .addClass('cbi-section-table-cell')
7009 .css('width', this.tabs[0].fields[j].options.width || '')
7010 .append(this.tabs[0].fields[j].label('caption')));
7011
7012 if (this.options.addremove !== false || this.options.sortable)
7013 thead.children().append($('<th />')
7014 .addClass('cbi-section-table-cell')
7015 .text(' '));
7016
7017 return thead;
7018 },
7019
7020 _render_table_row: function(sid, index)
7021 {
7022 var row = $('<tr />')
7023 .attr('data-luci2-sid', sid);
7024
7025 for (var j = 0; j < this.tabs[0].fields.length; j++)
7026 {
7027 row.append($('<td />')
7028 .css('width', this.tabs[0].fields[j].options.width || '')
7029 .append(this.tabs[0].fields[j].render(sid, true)));
7030 }
7031
7032 if (this.options.addremove !== false || this.options.sortable)
7033 {
7034 row.append($('<td />')
7035 .addClass('text-right')
7036 .append($('<div />')
7037 .addClass('btn-group')
7038 .append(this._render_sort(sid, index))
7039 .append(this._render_remove(sid, index))));
7040 }
7041
7042 return row;
7043 },
7044
7045 _render_table_body: function()
7046 {
7047 var s = this.sections();
7048
7049 var tbody = $('<tbody />');
7050
7051 if (s.length == 0)
7052 {
7053 var cols = this.tabs[0].fields.length;
7054
7055 if (this.options.addremove !== false || this.options.sortable)
7056 cols++;
7057
7058 tbody.append($('<tr />')
7059 .append($('<td />')
7060 .addClass('text-muted')
7061 .attr('colspan', cols)
7062 .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
7063 }
7064
7065 for (var i = 0; i < s.length; i++)
7066 {
7067 var sid = s[i]['.name'];
7068 var inst = this.instance[sid] = { tabs: [ ] };
7069
7070 tbody.append(this._render_table_row(sid, i));
7071 }
7072
7073 return tbody;
7074 },
7075
7076 _render_body: function(condensed)
7077 {
7078 return $('<table />')
7079 .addClass('table table-condensed table-hover')
7080 .append(this._render_table_head())
7081 .append(this._render_table_body());
7082 }
7083 });
7084
7085 this.cbi.NamedSection = this.cbi.TypedSection.extend({
7086 sections: function(cb)
7087 {
7088 var sa = [ ];
7089 var sl = L.uci.sections(this.map.uci_package);
7090
7091 for (var i = 0; i < sl.length; i++)
7092 if (sl[i]['.name'] == this.uci_type)
7093 {
7094 sa.push(sl[i]);
7095 break;
7096 }
7097
7098 if (typeof(cb) == 'function' && sa.length > 0)
7099 cb.call(this, sa[0]);
7100
7101 return sa;
7102 }
7103 });
7104
7105 this.cbi.SingleSection = this.cbi.NamedSection.extend({
7106 render: function()
7107 {
7108 this.instance = { };
7109 this.instance[this.uci_type] = { tabs: [ ] };
7110
7111 return this._render_section_body(this.uci_type, 0);
7112 }
7113 });
7114
7115 this.cbi.DummySection = this.cbi.TypedSection.extend({
7116 sections: function(cb)
7117 {
7118 if (typeof(cb) == 'function')
7119 cb.apply(this, [ { '.name': this.uci_type } ]);
7120
7121 return [ { '.name': this.uci_type } ];
7122 }
7123 });
7124
7125 this.cbi.Map = this.ui.AbstractWidget.extend({
7126 init: function(uci_package, options)
7127 {
7128 var self = this;
7129
7130 this.uci_package = uci_package;
7131 this.sections = [ ];
7132 this.options = L.defaults(options, {
7133 save: function() { },
7134 prepare: function() { }
7135 });
7136 },
7137
7138 _load_cb: function()
7139 {
7140 var deferreds = [ L.deferrable(this.options.prepare()) ];
7141
7142 for (var i = 0; i < this.sections.length; i++)
7143 {
7144 for (var f in this.sections[i].fields)
7145 {
7146 if (typeof(this.sections[i].fields[f].load) != 'function')
7147 continue;
7148
7149 var s = this.sections[i].sections();
7150 for (var j = 0; j < s.length; j++)
7151 {
7152 var rv = this.sections[i].fields[f].load(s[j]['.name']);
7153 if (L.isDeferred(rv))
7154 deferreds.push(rv);
7155 }
7156 }
7157 }
7158
7159 return $.when.apply($, deferreds);
7160 },
7161
7162 load: function()
7163 {
7164 var self = this;
7165 var packages = { };
7166
7167 for (var i = 0; i < this.sections.length; i++)
7168 this.sections[i].ucipackages(packages);
7169
7170 packages[this.uci_package] = true;
7171
7172 for (var pkg in packages)
7173 if (!L.uci.writable(pkg))
7174 this.options.readonly = true;
7175
7176 return L.uci.load(L.toArray(packages)).then(function() {
7177 return self._load_cb();
7178 });
7179 },
7180
7181 _ev_tab: function(ev)
7182 {
7183 var self = ev.data.self;
7184
7185 self.validate();
7186 self.active_tab = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
7187 },
7188
7189 _ev_apply: function(ev)
7190 {
7191 var self = ev.data.self;
7192
7193 self.trigger('apply', ev);
7194 },
7195
7196 _ev_save: function(ev)
7197 {
7198 var self = ev.data.self;
7199
7200 self.send().then(function() {
7201 self.trigger('save', ev);
7202 });
7203 },
7204
7205 _ev_reset: function(ev)
7206 {
7207 var self = ev.data.self;
7208
7209 self.trigger('reset', ev);
7210 self.reset();
7211 },
7212
7213 _render_tab_head: function(tab_index)
7214 {
7215 var section = this.sections[tab_index];
7216 var cur = this.active_tab || 0;
7217
7218 var tabh = $('<li />')
7219 .append($('<a />')
7220 .attr('id', section.id('sectiontab'))
7221 .attr('href', '#' + section.id('section'))
7222 .attr('data-toggle', 'tab')
7223 .attr('data-luci2-tab-index', tab_index)
7224 .text(section.label('caption') + ' ')
7225 .append($('<span />')
7226 .addClass('badge'))
7227 .on('shown.bs.tab', { self: this }, this._ev_tab));
7228
7229 if (cur == tab_index)
7230 tabh.addClass('active');
7231
7232 return tabh;
7233 },
7234
7235 _render_tab_body: function(tab_index)
7236 {
7237 var section = this.sections[tab_index];
7238 var desc = section.label('description');
7239 var cur = this.active_tab || 0;
7240
7241 var tabb = $('<div />')
7242 .addClass('tab-pane')
7243 .attr('id', section.id('section'))
7244 .attr('data-luci2-tab-index', tab_index);
7245
7246 if (cur == tab_index)
7247 tabb.addClass('active');
7248
7249 if (desc)
7250 tabb.append($('<p />')
7251 .text(desc));
7252
7253 var s = section.render(this.options.tabbed);
7254
7255 if (this.options.readonly || section.options.readonly)
7256 s.find('input, select, button, img.cbi-button').attr('disabled', true);
7257
7258 tabb.append(s);
7259
7260 return tabb;
7261 },
7262
7263 _render_body: function()
7264 {
7265 var tabs = $('<ul />')
7266 .addClass('nav nav-tabs');
7267
7268 var body = $('<div />')
7269 .append(tabs);
7270
7271 for (var i = 0; i < this.sections.length; i++)
7272 {
7273 tabs.append(this._render_tab_head(i));
7274 body.append(this._render_tab_body(i));
7275 }
7276
7277 if (this.options.tabbed)
7278 body.addClass('tab-content');
7279 else
7280 tabs.hide();
7281
7282 return body;
7283 },
7284
7285 _render_footer: function()
7286 {
7287 var evdata = {
7288 self: this
7289 };
7290
7291 return $('<div />')
7292 .addClass('panel panel-default panel-body text-right')
7293 .append($('<div />')
7294 .addClass('btn-group')
7295 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
7296 .click(evdata, this._ev_apply))
7297 .append(L.ui.button(L.tr('Save'), 'default')
7298 .click(evdata, this._ev_save))
7299 .append(L.ui.button(L.tr('Reset'), 'default')
7300 .click(evdata, this._ev_reset)));
7301 },
7302
7303 render: function()
7304 {
7305 var map = $('<form />');
7306
7307 if (typeof(this.options.caption) == 'string')
7308 map.append($('<h2 />')
7309 .text(this.options.caption));
7310
7311 if (typeof(this.options.description) == 'string')
7312 map.append($('<p />')
7313 .text(this.options.description));
7314
7315 map.append(this._render_body());
7316
7317 if (this.options.pageaction !== false)
7318 map.append(this._render_footer());
7319
7320 return map;
7321 },
7322
7323 finish: function()
7324 {
7325 for (var i = 0; i < this.sections.length; i++)
7326 this.sections[i].finish();
7327
7328 this.validate();
7329 },
7330
7331 redraw: function()
7332 {
7333 this.target.hide().empty().append(this.render());
7334 this.finish();
7335 this.target.show();
7336 },
7337
7338 section: function(widget, uci_type, options)
7339 {
7340 var w = widget ? new widget(uci_type, options) : null;
7341
7342 if (!(w instanceof L.cbi.AbstractSection))
7343 throw 'Widget must be an instance of AbstractSection';
7344
7345 w.map = this;
7346 w.index = this.sections.length;
7347
7348 this.sections.push(w);
7349 return w;
7350 },
7351
7352 formvalue: function()
7353 {
7354 var rv = { };
7355
7356 for (var i = 0; i < this.sections.length; i++)
7357 {
7358 var sids = this.sections[i].formvalue();
7359 for (var sid in sids)
7360 {
7361 var s = rv[sid] || (rv[sid] = { });
7362 $.extend(s, sids[sid]);
7363 }
7364 }
7365
7366 return rv;
7367 },
7368
7369 add: function(conf, type, name)
7370 {
7371 return L.uci.add(conf, type, name);
7372 },
7373
7374 remove: function(conf, sid)
7375 {
7376 return L.uci.remove(conf, sid);
7377 },
7378
7379 get: function(conf, sid, opt)
7380 {
7381 return L.uci.get(conf, sid, opt);
7382 },
7383
7384 set: function(conf, sid, opt, val)
7385 {
7386 return L.uci.set(conf, sid, opt, val);
7387 },
7388
7389 validate: function()
7390 {
7391 var rv = true;
7392
7393 for (var i = 0; i < this.sections.length; i++)
7394 {
7395 if (!this.sections[i].validate())
7396 rv = false;
7397 }
7398
7399 return rv;
7400 },
7401
7402 save: function()
7403 {
7404 var self = this;
7405
7406 if (self.options.readonly)
7407 return L.deferrable();
7408
7409 var deferreds = [ ];
7410
7411 for (var i = 0; i < self.sections.length; i++)
7412 {
7413 if (self.sections[i].options.readonly)
7414 continue;
7415
7416 for (var f in self.sections[i].fields)
7417 {
7418 if (typeof(self.sections[i].fields[f].save) != 'function')
7419 continue;
7420
7421 var s = self.sections[i].sections();
7422 for (var j = 0; j < s.length; j++)
7423 {
7424 var rv = self.sections[i].fields[f].save(s[j]['.name']);
7425 if (L.isDeferred(rv))
7426 deferreds.push(rv);
7427 }
7428 }
7429 }
7430
7431 return $.when.apply($, deferreds).then(function() {
7432 return L.deferrable(self.options.save());
7433 });
7434 },
7435
7436 send: function()
7437 {
7438 if (!this.validate())
7439 return L.deferrable();
7440
7441 var self = this;
7442
7443 L.ui.saveScrollTop();
7444 L.ui.loading(true);
7445
7446 return this.save().then(function() {
7447 return L.uci.save();
7448 }).then(function() {
7449 return L.ui.updateChanges();
7450 }).then(function() {
7451 return self.load();
7452 }).then(function() {
7453 self.redraw();
7454 self = null;
7455
7456 L.ui.loading(false);
7457 L.ui.restoreScrollTop();
7458 });
7459 },
7460
7461 revert: function()
7462 {
7463 var packages = { };
7464
7465 for (var i = 0; i < this.sections.length; i++)
7466 this.sections[i].ucipackages(packages);
7467
7468 packages[this.uci_package] = true;
7469
7470 L.uci.unload(L.toArray(packages));
7471 },
7472
7473 reset: function()
7474 {
7475 var self = this;
7476
7477 self.revert();
7478
7479 return self.insertInto(self.target);
7480 },
7481
7482 insertInto: function(id)
7483 {
7484 var self = this;
7485 self.target = $(id);
7486
7487 L.ui.loading(true);
7488 self.target.hide();
7489
7490 return self.load().then(function() {
7491 self.target.empty().append(self.render());
7492 self.finish();
7493 self.target.show();
7494 self = null;
7495 L.ui.loading(false);
7496 });
7497 }
7498 });
7499
7500 this.cbi.Modal = this.cbi.Map.extend({
7501 _ev_apply: function(ev)
7502 {
7503 var self = ev.data.self;
7504
7505 self.trigger('apply', ev);
7506 },
7507
7508 _ev_save: function(ev)
7509 {
7510 var self = ev.data.self;
7511
7512 self.send().then(function() {
7513 self.trigger('save', ev);
7514 self.close();
7515 });
7516 },
7517
7518 _ev_reset: function(ev)
7519 {
7520 var self = ev.data.self;
7521
7522 self.trigger('close', ev);
7523 self.revert();
7524 self.close();
7525 },
7526
7527 _render_footer: function()
7528 {
7529 var evdata = {
7530 self: this
7531 };
7532
7533 return $('<div />')
7534 .addClass('btn-group')
7535 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
7536 .click(evdata, this._ev_apply))
7537 .append(L.ui.button(L.tr('Save'), 'default')
7538 .click(evdata, this._ev_save))
7539 .append(L.ui.button(L.tr('Cancel'), 'default')
7540 .click(evdata, this._ev_reset));
7541 },
7542
7543 render: function()
7544 {
7545 var modal = L.ui.dialog(this.label('caption'), null, { wide: true });
7546 var map = $('<form />');
7547
7548 var desc = this.label('description');
7549 if (desc)
7550 map.append($('<p />').text(desc));
7551
7552 map.append(this._render_body());
7553
7554 modal.find('.modal-body').append(map);
7555 modal.find('.modal-footer').append(this._render_footer());
7556
7557 return modal;
7558 },
7559
7560 redraw: function()
7561 {
7562 this.render();
7563 this.finish();
7564 },
7565
7566 show: function()
7567 {
7568 var self = this;
7569
7570 L.ui.loading(true);
7571
7572 return self.load().then(function() {
7573 self.render();
7574 self.finish();
7575
7576 L.ui.loading(false);
7577 });
7578 },
7579
7580 close: function()
7581 {
7582 L.ui.dialog(false);
7583 }
7584 });
7585 };