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