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