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