luci2: add network protocol support extensions
[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 };