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