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