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