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