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