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