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