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