5947791600a9ace955dade058bbd8849ec29a2ce
[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                 setDevices: function(devs)
2741                 {
2742                         var dev = this.getPhysdev();
2743                         var old_devs = [ ];
2744                         var changed = false;
2745
2746                         if (dev && dev.isBridge())
2747                                 old_devs = this.getSubdevices();
2748                         else if (dev)
2749                                 old_devs = [ dev ];
2750
2751                         if (old_devs.length != devs.length)
2752                                 changed = true;
2753                         else
2754                                 for (var i = 0; i < old_devs.length; i++)
2755                                 {
2756                                         var dev = devs[i];
2757
2758                                         if (dev instanceof L.NetworkModel.Device)
2759                                                 dev = dev.name();
2760
2761                                         if (!dev || old_devs[i].name() != dev)
2762                                         {
2763                                                 changed = true;
2764                                                 break;
2765                                         }
2766                                 }
2767
2768                         if (changed)
2769                         {
2770                                 for (var i = 0; i < old_devs.length; i++)
2771                                         old_devs[i].removeFromInterface(this);
2772
2773                                 for (var i = 0; i < devs.length; i++)
2774                                 {
2775                                         var dev = devs[i];
2776
2777                                         if (!(dev instanceof L.NetworkModel.Device))
2778                                                 dev = L.NetworkModel.getDevice(dev);
2779
2780                                         if (dev)
2781                                                 dev.attachToInterface(this);
2782                                 }
2783                         }
2784                 },
2785
2786                 changeProtocol: function(proto)
2787                 {
2788                         var pr = L.NetworkModel._protos[proto];
2789
2790                         if (!pr)
2791                                 return;
2792
2793                         for (var opt in (this.get() || { }))
2794                         {
2795                                 switch (opt)
2796                                 {
2797                                 case 'type':
2798                                 case 'ifname':
2799                                 case 'macaddr':
2800                                         if (pr.virtual)
2801                                                 this.set(opt, undefined);
2802                                         break;
2803
2804                                 case 'auto':
2805                                 case 'mtu':
2806                                         break;
2807
2808                                 case 'proto':
2809                                         this.set(opt, pr.protocol);
2810                                         break;
2811
2812                                 default:
2813                                         this.set(opt, undefined);
2814                                         break;
2815                                 }
2816                         }
2817                 },
2818
2819                 createForm: function(mapwidget)
2820                 {
2821                         var self = this;
2822                         var proto = self.getProtocol();
2823                         var device = self.getDevice();
2824
2825                         if (!mapwidget)
2826                                 mapwidget = L.cbi.Map;
2827
2828                         var map = new mapwidget('network', {
2829                                 caption:     L.tr('Configure "%s"').format(self.name())
2830                         });
2831
2832                         var section = map.section(L.cbi.SingleSection, self.name(), {
2833                                 anonymous:   true
2834                         });
2835
2836                         section.tab({
2837                                 id:      'general',
2838                                 caption: L.tr('General Settings')
2839                         });
2840
2841                         section.tab({
2842                                 id:      'advanced',
2843                                 caption: L.tr('Advanced Settings')
2844                         });
2845
2846                         section.tab({
2847                                 id:      'ipv6',
2848                                 caption: L.tr('IPv6')
2849                         });
2850
2851                         section.tab({
2852                                 id:      'physical',
2853                                 caption: L.tr('Physical Settings')
2854                         });
2855
2856
2857                         section.taboption('general', L.cbi.CheckboxValue, 'auto', {
2858                                 caption:     L.tr('Start on boot'),
2859                                 optional:    true,
2860                                 initial:     true
2861                         });
2862
2863                         var pr = section.taboption('general', L.cbi.ListValue, 'proto', {
2864                                 caption:     L.tr('Protocol')
2865                         });
2866
2867                         pr.ucivalue = function(sid) {
2868                                 return self.get('proto') || 'none';
2869                         };
2870
2871                         var ok = section.taboption('general', L.cbi.ButtonValue, '_confirm', {
2872                                 caption:     L.tr('Really switch?'),
2873                                 description: L.tr('Changing the protocol will clear all configuration for this interface!'),
2874                                 text:        L.tr('Change protocol')
2875                         });
2876
2877                         ok.on('click', function(ev) {
2878                                 self.changeProtocol(pr.formvalue(ev.data.sid));
2879                                 self.createForm(mapwidget).show();
2880                         });
2881
2882                         var protos = L.NetworkModel.getProtocols();
2883
2884                         for (var i = 0; i < protos.length; i++)
2885                                 pr.value(protos[i].name, protos[i].description);
2886
2887                         proto.populateForm(section, self);
2888
2889                         if (!proto.virtual)
2890                         {
2891                                 var br = section.taboption('physical', L.cbi.CheckboxValue, 'type', {
2892                                         caption:     L.tr('Network bridge'),
2893                                         description: L.tr('Merges multiple devices into one logical bridge'),
2894                                         optional:    true,
2895                                         enabled:     'bridge',
2896                                         disabled:    '',
2897                                         initial:     ''
2898                                 });
2899
2900                                 section.taboption('physical', L.cbi.DeviceList, '__iface_multi', {
2901                                         caption:     L.tr('Devices'),
2902                                         multiple:    true,
2903                                         bridges:     false
2904                                 }).depends('type', true);
2905
2906                                 section.taboption('physical', L.cbi.DeviceList, '__iface_single', {
2907                                         caption:     L.tr('Device'),
2908                                         multiple:    false,
2909                                         bridges:     true
2910                                 }).depends('type', false);
2911
2912                                 var mac = section.taboption('physical', L.cbi.InputValue, 'macaddr', {
2913                                         caption:     L.tr('Override MAC'),
2914                                         optional:    true,
2915                                         placeholder: device ? device.getMACAddress() : undefined,
2916                                         datatype:    'macaddr'
2917                                 })
2918
2919                                 mac.ucivalue = function(sid)
2920                                 {
2921                                         if (device)
2922                                                 return device.get('macaddr');
2923
2924                                         return this.callSuper('ucivalue', sid);
2925                                 };
2926
2927                                 mac.save = function(sid)
2928                                 {
2929                                         if (!this.changed(sid))
2930                                                 return false;
2931
2932                                         if (device)
2933                                                 device.set('macaddr', this.formvalue(sid));
2934                                         else
2935                                                 this.callSuper('set', sid);
2936
2937                                         return true;
2938                                 };
2939                         }
2940
2941                         section.taboption('physical', L.cbi.InputValue, 'mtu', {
2942                                 caption:     L.tr('Override MTU'),
2943                                 optional:    true,
2944                                 placeholder: device ? device.getMTU() : undefined,
2945                                 datatype:    'range(1, 9000)'
2946                         });
2947
2948                         section.taboption('physical', L.cbi.InputValue, 'metric', {
2949                                 caption:     L.tr('Override Metric'),
2950                                 optional:    true,
2951                                 placeholder: 0,
2952                                 datatype:    'uinteger'
2953                         });
2954
2955                         for (var field in section.fields)
2956                         {
2957                                 switch (field)
2958                                 {
2959                                 case 'proto':
2960                                         break;
2961
2962                                 case '_confirm':
2963                                         for (var i = 0; i < protos.length; i++)
2964                                                 if (protos[i].name != (this.get('proto') || 'none'))
2965                                                         section.fields[field].depends('proto', protos[i].name);
2966                                         break;
2967
2968                                 default:
2969                                         section.fields[field].depends('proto', this.get('proto') || 'none', true);
2970                                         break;
2971                                 }
2972                         }
2973
2974                         return map;
2975                 }
2976         });
2977
2978         this.NetworkModel.Protocol = this.NetworkModel.Interface.extend({
2979                 description: '__unknown__',
2980                 tunnel:      false,
2981                 virtual:     false,
2982
2983                 populateForm: function(section, iface)
2984                 {
2985
2986                 }
2987         });
2988
2989         this.system = {
2990                 getSystemInfo: L.rpc.declare({
2991                         object: 'system',
2992                         method: 'info',
2993                         expect: { '': { } }
2994                 }),
2995
2996                 getBoardInfo: L.rpc.declare({
2997                         object: 'system',
2998                         method: 'board',
2999                         expect: { '': { } }
3000                 }),
3001
3002                 getDiskInfo: L.rpc.declare({
3003                         object: 'luci2.system',
3004                         method: 'diskfree',
3005                         expect: { '': { } }
3006                 }),
3007
3008                 getInfo: function(cb)
3009                 {
3010                         L.rpc.batch();
3011
3012                         this.getSystemInfo();
3013                         this.getBoardInfo();
3014                         this.getDiskInfo();
3015
3016                         return L.rpc.flush().then(function(info) {
3017                                 var rv = { };
3018
3019                                 $.extend(rv, info[0]);
3020                                 $.extend(rv, info[1]);
3021                                 $.extend(rv, info[2]);
3022
3023                                 return rv;
3024                         });
3025                 },
3026
3027
3028                 initList: L.rpc.declare({
3029                         object: 'luci2.system',
3030                         method: 'init_list',
3031                         expect: { initscripts: [ ] },
3032                         filter: function(data) {
3033                                 data.sort(function(a, b) { return (a.start || 0) - (b.start || 0) });
3034                                 return data;
3035                         }
3036                 }),
3037
3038                 initEnabled: function(init, cb)
3039                 {
3040                         return this.initList().then(function(list) {
3041                                 for (var i = 0; i < list.length; i++)
3042                                         if (list[i].name == init)
3043                                                 return !!list[i].enabled;
3044
3045                                 return false;
3046                         });
3047                 },
3048
3049                 initRun: L.rpc.declare({
3050                         object: 'luci2.system',
3051                         method: 'init_action',
3052                         params: [ 'name', 'action' ],
3053                         filter: function(data) {
3054                                 return (data == 0);
3055                         }
3056                 }),
3057
3058                 initStart:   function(init, cb) { return L.system.initRun(init, 'start',   cb) },
3059                 initStop:    function(init, cb) { return L.system.initRun(init, 'stop',    cb) },
3060                 initRestart: function(init, cb) { return L.system.initRun(init, 'restart', cb) },
3061                 initReload:  function(init, cb) { return L.system.initRun(init, 'reload',  cb) },
3062                 initEnable:  function(init, cb) { return L.system.initRun(init, 'enable',  cb) },
3063                 initDisable: function(init, cb) { return L.system.initRun(init, 'disable', cb) },
3064
3065
3066                 performReboot: L.rpc.declare({
3067                         object: 'luci2.system',
3068                         method: 'reboot'
3069                 })
3070         };
3071
3072         this.session = {
3073
3074                 login: L.rpc.declare({
3075                         object: 'session',
3076                         method: 'login',
3077                         params: [ 'username', 'password' ],
3078                         expect: { '': { } }
3079                 }),
3080
3081                 access: L.rpc.declare({
3082                         object: 'session',
3083                         method: 'access',
3084                         params: [ 'scope', 'object', 'function' ],
3085                         expect: { access: false }
3086                 }),
3087
3088                 isAlive: function()
3089                 {
3090                         return L.session.access('ubus', 'session', 'access');
3091                 },
3092
3093                 startHeartbeat: function()
3094                 {
3095                         this._hearbeatInterval = window.setInterval(function() {
3096                                 L.session.isAlive().then(function(alive) {
3097                                         if (!alive)
3098                                         {
3099                                                 L.session.stopHeartbeat();
3100                                                 L.ui.login(true);
3101                                         }
3102
3103                                 });
3104                         }, L.globals.timeout * 2);
3105                 },
3106
3107                 stopHeartbeat: function()
3108                 {
3109                         if (typeof(this._hearbeatInterval) != 'undefined')
3110                         {
3111                                 window.clearInterval(this._hearbeatInterval);
3112                                 delete this._hearbeatInterval;
3113                         }
3114                 },
3115
3116
3117                 _acls: { },
3118
3119                 _fetch_acls: L.rpc.declare({
3120                         object: 'session',
3121                         method: 'access',
3122                         expect: { '': { } }
3123                 }),
3124
3125                 _fetch_acls_cb: function(acls)
3126                 {
3127                         L.session._acls = acls;
3128                 },
3129
3130                 updateACLs: function()
3131                 {
3132                         return L.session._fetch_acls()
3133                                 .then(L.session._fetch_acls_cb);
3134                 },
3135
3136                 hasACL: function(scope, object, func)
3137                 {
3138                         var acls = L.session._acls;
3139
3140                         if (typeof(func) == 'undefined')
3141                                 return (acls && acls[scope] && acls[scope][object]);
3142
3143                         if (acls && acls[scope] && acls[scope][object])
3144                                 for (var i = 0; i < acls[scope][object].length; i++)
3145                                         if (acls[scope][object][i] == func)
3146                                                 return true;
3147
3148                         return false;
3149                 }
3150         };
3151
3152         this.ui = {
3153
3154                 saveScrollTop: function()
3155                 {
3156                         this._scroll_top = $(document).scrollTop();
3157                 },
3158
3159                 restoreScrollTop: function()
3160                 {
3161                         if (typeof(this._scroll_top) == 'undefined')
3162                                 return;
3163
3164                         $(document).scrollTop(this._scroll_top);
3165
3166                         delete this._scroll_top;
3167                 },
3168
3169                 loading: function(enable)
3170                 {
3171                         var win = $(window);
3172                         var body = $('body');
3173
3174                         var state = L.ui._loading || (L.ui._loading = {
3175                                 modal: $('<div />')
3176                                         .css('z-index', 2000)
3177                                         .addClass('modal fade')
3178                                         .append($('<div />')
3179                                                 .addClass('modal-dialog')
3180                                                 .append($('<div />')
3181                                                         .addClass('modal-content luci2-modal-loader')
3182                                                         .append($('<div />')
3183                                                                 .addClass('modal-body')
3184                                                                 .text(L.tr('Loading data…')))))
3185                                         .appendTo(body)
3186                                         .modal({
3187                                                 backdrop: 'static',
3188                                                 keyboard: false
3189                                         })
3190                         });
3191
3192                         state.modal.modal(enable ? 'show' : 'hide');
3193                 },
3194
3195                 dialog: function(title, content, options)
3196                 {
3197                         var win = $(window);
3198                         var body = $('body');
3199
3200                         var state = L.ui._dialog || (L.ui._dialog = {
3201                                 dialog: $('<div />')
3202                                         .addClass('modal fade')
3203                                         .append($('<div />')
3204                                                 .addClass('modal-dialog')
3205                                                 .append($('<div />')
3206                                                         .addClass('modal-content')
3207                                                         .append($('<div />')
3208                                                                 .addClass('modal-header')
3209                                                                 .append('<h4 />')
3210                                                                         .addClass('modal-title'))
3211                                                         .append($('<div />')
3212                                                                 .addClass('modal-body'))
3213                                                         .append($('<div />')
3214                                                                 .addClass('modal-footer')
3215                                                                 .append(L.ui.button(L.tr('Close'), 'primary')
3216                                                                         .click(function() {
3217                                                                                 $(this).parents('div.modal').modal('hide');
3218                                                                         })))))
3219                                         .appendTo(body)
3220                         });
3221
3222                         if (typeof(options) != 'object')
3223                                 options = { };
3224
3225                         if (title === false)
3226                         {
3227                                 state.dialog.modal('hide');
3228
3229                                 return state.dialog;
3230                         }
3231
3232                         var cnt = state.dialog.children().children().children('div.modal-body');
3233                         var ftr = state.dialog.children().children().children('div.modal-footer');
3234
3235                         ftr.empty().show();
3236
3237                         if (options.style == 'confirm')
3238                         {
3239                                 ftr.append(L.ui.button(L.tr('Ok'), 'primary')
3240                                         .click(options.confirm || function() { L.ui.dialog(false) }));
3241
3242                                 ftr.append(L.ui.button(L.tr('Cancel'), 'default')
3243                                         .click(options.cancel || function() { L.ui.dialog(false) }));
3244                         }
3245                         else if (options.style == 'close')
3246                         {
3247                                 ftr.append(L.ui.button(L.tr('Close'), 'primary')
3248                                         .click(options.close || function() { L.ui.dialog(false) }));
3249                         }
3250                         else if (options.style == 'wait')
3251                         {
3252                                 ftr.append(L.ui.button(L.tr('Close'), 'primary')
3253                                         .attr('disabled', true));
3254                         }
3255
3256                         if (options.wide)
3257                         {
3258                                 state.dialog.addClass('wide');
3259                         }
3260                         else
3261                         {
3262                                 state.dialog.removeClass('wide');
3263                         }
3264
3265                         state.dialog.find('h4:first').text(title);
3266                         state.dialog.modal('show');
3267
3268                         cnt.empty().append(content);
3269
3270                         return state.dialog;
3271                 },
3272
3273                 upload: function(title, content, options)
3274                 {
3275                         var state = L.ui._upload || (L.ui._upload = {
3276                                 form: $('<form />')
3277                                         .attr('method', 'post')
3278                                         .attr('action', '/cgi-bin/luci-upload')
3279                                         .attr('enctype', 'multipart/form-data')
3280                                         .attr('target', 'cbi-fileupload-frame')
3281                                         .append($('<p />'))
3282                                         .append($('<input />')
3283                                                 .attr('type', 'hidden')
3284                                                 .attr('name', 'sessionid'))
3285                                         .append($('<input />')
3286                                                 .attr('type', 'hidden')
3287                                                 .attr('name', 'filename'))
3288                                         .append($('<input />')
3289                                                 .attr('type', 'file')
3290                                                 .attr('name', 'filedata')
3291                                                 .addClass('cbi-input-file'))
3292                                         .append($('<div />')
3293                                                 .css('width', '100%')
3294                                                 .addClass('progress progress-striped active')
3295                                                 .append($('<div />')
3296                                                         .addClass('progress-bar')
3297                                                         .css('width', '100%')))
3298                                         .append($('<iframe />')
3299                                                 .addClass('pull-right')
3300                                                 .attr('name', 'cbi-fileupload-frame')
3301                                                 .css('width', '1px')
3302                                                 .css('height', '1px')
3303                                                 .css('visibility', 'hidden')),
3304
3305                                 finish_cb: function(ev) {
3306                                         $(this).off('load');
3307
3308                                         var body = (this.contentDocument || this.contentWindow.document).body;
3309                                         if (body.firstChild.tagName.toLowerCase() == 'pre')
3310                                                 body = body.firstChild;
3311
3312                                         var json;
3313                                         try {
3314                                                 json = $.parseJSON(body.innerHTML);
3315                                         } catch(e) {
3316                                                 json = {
3317                                                         message: L.tr('Invalid server response received'),
3318                                                         error: [ -1, L.tr('Invalid data') ]
3319                                                 };
3320                                         };
3321
3322                                         if (json.error)
3323                                         {
3324                                                 L.ui.dialog(L.tr('File upload'), [
3325                                                         $('<p />').text(L.tr('The file upload failed with the server response below:')),
3326                                                         $('<pre />').addClass('alert-message').text(json.message || json.error[1]),
3327                                                         $('<p />').text(L.tr('In case of network problems try uploading the file again.'))
3328                                                 ], { style: 'close' });
3329                                         }
3330                                         else if (typeof(state.success_cb) == 'function')
3331                                         {
3332                                                 state.success_cb(json);
3333                                         }
3334                                 },
3335
3336                                 confirm_cb: function() {
3337                                         var f = state.form.find('.cbi-input-file');
3338                                         var b = state.form.find('.progress');
3339                                         var p = state.form.find('p');
3340
3341                                         if (!f.val())
3342                                                 return;
3343
3344                                         state.form.find('iframe').on('load', state.finish_cb);
3345                                         state.form.submit();
3346
3347                                         f.hide();
3348                                         b.show();
3349                                         p.text(L.tr('File upload in progress â€¦'));
3350
3351                                         state.form.parent().parent().find('button').prop('disabled', true);
3352                                 }
3353                         });
3354
3355                         state.form.find('.progress').hide();
3356                         state.form.find('.cbi-input-file').val('').show();
3357                         state.form.find('p').text(content || L.tr('Select the file to upload and press "%s" to proceed.').format(L.tr('Ok')));
3358
3359                         state.form.find('[name=sessionid]').val(L.globals.sid);
3360                         state.form.find('[name=filename]').val(options.filename);
3361
3362                         state.success_cb = options.success;
3363
3364                         L.ui.dialog(title || L.tr('File upload'), state.form, {
3365                                 style: 'confirm',
3366                                 confirm: state.confirm_cb
3367                         });
3368                 },
3369
3370                 reconnect: function()
3371                 {
3372                         var protocols = (location.protocol == 'https:') ? [ 'http', 'https' ] : [ 'http' ];
3373                         var ports     = (location.protocol == 'https:') ? [ 80, location.port || 443 ] : [ location.port || 80 ];
3374                         var address   = location.hostname.match(/^[A-Fa-f0-9]*:[A-Fa-f0-9:]+$/) ? '[' + location.hostname + ']' : location.hostname;
3375                         var images    = $();
3376                         var interval, timeout;
3377
3378                         L.ui.dialog(
3379                                 L.tr('Waiting for device'), [
3380                                         $('<p />').text(L.tr('Please stand by while the device is reconfiguring â€¦')),
3381                                         $('<div />')
3382                                                 .css('width', '100%')
3383                                                 .addClass('progressbar')
3384                                                 .addClass('intermediate')
3385                                                 .append($('<div />')
3386                                                         .css('width', '100%'))
3387                                 ], { style: 'wait' }
3388                         );
3389
3390                         for (var i = 0; i < protocols.length; i++)
3391                                 images = images.add($('<img />').attr('url', protocols[i] + '://' + address + ':' + ports[i]));
3392
3393                         //L.network.getNetworkStatus(function(s) {
3394                         //      for (var i = 0; i < protocols.length; i++)
3395                         //      {
3396                         //              for (var j = 0; j < s.length; j++)
3397                         //              {
3398                         //                      for (var k = 0; k < s[j]['ipv4-address'].length; k++)
3399                         //                              images = images.add($('<img />').attr('url', protocols[i] + '://' + s[j]['ipv4-address'][k].address + ':' + ports[i]));
3400                         //
3401                         //                      for (var l = 0; l < s[j]['ipv6-address'].length; l++)
3402                         //                              images = images.add($('<img />').attr('url', protocols[i] + '://[' + s[j]['ipv6-address'][l].address + ']:' + ports[i]));
3403                         //              }
3404                         //      }
3405                         //}).then(function() {
3406                                 images.on('load', function() {
3407                                         var url = this.getAttribute('url');
3408                                         L.session.isAlive().then(function(access) {
3409                                                 if (access)
3410                                                 {
3411                                                         window.clearTimeout(timeout);
3412                                                         window.clearInterval(interval);
3413                                                         L.ui.dialog(false);
3414                                                         images = null;
3415                                                 }
3416                                                 else
3417                                                 {
3418                                                         location.href = url;
3419                                                 }
3420                                         });
3421                                 });
3422
3423                                 interval = window.setInterval(function() {
3424                                         images.each(function() {
3425                                                 this.setAttribute('src', this.getAttribute('url') + L.globals.resource + '/icons/loading.gif?r=' + Math.random());
3426                                         });
3427                                 }, 5000);
3428
3429                                 timeout = window.setTimeout(function() {
3430                                         window.clearInterval(interval);
3431                                         images.off('load');
3432
3433                                         L.ui.dialog(
3434                                                 L.tr('Device not responding'),
3435                                                 L.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'),
3436                                                 { style: 'close' }
3437                                         );
3438                                 }, 180000);
3439                         //});
3440                 },
3441
3442                 login: function(invalid)
3443                 {
3444                         var state = L.ui._login || (L.ui._login = {
3445                                 form: $('<form />')
3446                                         .attr('target', '')
3447                                         .attr('method', 'post')
3448                                         .append($('<p />')
3449                                                 .addClass('alert-message')
3450                                                 .text(L.tr('Wrong username or password given!')))
3451                                         .append($('<p />')
3452                                                 .append($('<label />')
3453                                                         .text(L.tr('Username'))
3454                                                         .append($('<br />'))
3455                                                         .append($('<input />')
3456                                                                 .attr('type', 'text')
3457                                                                 .attr('name', 'username')
3458                                                                 .attr('value', 'root')
3459                                                                 .addClass('form-control')
3460                                                                 .keypress(function(ev) {
3461                                                                         if (ev.which == 10 || ev.which == 13)
3462                                                                                 state.confirm_cb();
3463                                                                 }))))
3464                                         .append($('<p />')
3465                                                 .append($('<label />')
3466                                                         .text(L.tr('Password'))
3467                                                         .append($('<br />'))
3468                                                         .append($('<input />')
3469                                                                 .attr('type', 'password')
3470                                                                 .attr('name', 'password')
3471                                                                 .addClass('form-control')
3472                                                                 .keypress(function(ev) {
3473                                                                         if (ev.which == 10 || ev.which == 13)
3474                                                                                 state.confirm_cb();
3475                                                                 }))))
3476                                         .append($('<p />')
3477                                                 .text(L.tr('Enter your username and password above, then click "%s" to proceed.').format(L.tr('Ok')))),
3478
3479                                 response_cb: function(response) {
3480                                         if (!response.ubus_rpc_session)
3481                                         {
3482                                                 L.ui.login(true);
3483                                         }
3484                                         else
3485                                         {
3486                                                 L.globals.sid = response.ubus_rpc_session;
3487                                                 L.setHash('id', L.globals.sid);
3488                                                 L.session.startHeartbeat();
3489                                                 L.ui.dialog(false);
3490                                                 state.deferred.resolve();
3491                                         }
3492                                 },
3493
3494                                 confirm_cb: function() {
3495                                         var u = state.form.find('[name=username]').val();
3496                                         var p = state.form.find('[name=password]').val();
3497
3498                                         if (!u)
3499                                                 return;
3500
3501                                         L.ui.dialog(
3502                                                 L.tr('Logging in'), [
3503                                                         $('<p />').text(L.tr('Log in in progress â€¦')),
3504                                                         $('<div />')
3505                                                                 .css('width', '100%')
3506                                                                 .addClass('progressbar')
3507                                                                 .addClass('intermediate')
3508                                                                 .append($('<div />')
3509                                                                         .css('width', '100%'))
3510                                                 ], { style: 'wait' }
3511                                         );
3512
3513                                         L.globals.sid = '00000000000000000000000000000000';
3514                                         L.session.login(u, p).then(state.response_cb);
3515                                 }
3516                         });
3517
3518                         if (!state.deferred || state.deferred.state() != 'pending')
3519                                 state.deferred = $.Deferred();
3520
3521                         /* try to find sid from hash */
3522                         var sid = L.getHash('id');
3523                         if (sid && sid.match(/^[a-f0-9]{32}$/))
3524                         {
3525                                 L.globals.sid = sid;
3526                                 L.session.isAlive().then(function(access) {
3527                                         if (access)
3528                                         {
3529                                                 L.session.startHeartbeat();
3530                                                 state.deferred.resolve();
3531                                         }
3532                                         else
3533                                         {
3534                                                 L.setHash('id', undefined);
3535                                                 L.ui.login();
3536                                         }
3537                                 });
3538
3539                                 return state.deferred;
3540                         }
3541
3542                         if (invalid)
3543                                 state.form.find('.alert-message').show();
3544                         else
3545                                 state.form.find('.alert-message').hide();
3546
3547                         L.ui.dialog(L.tr('Authorization Required'), state.form, {
3548                                 style: 'confirm',
3549                                 confirm: state.confirm_cb
3550                         });
3551
3552                         state.form.find('[name=password]').focus();
3553
3554                         return state.deferred;
3555                 },
3556
3557                 cryptPassword: L.rpc.declare({
3558                         object: 'luci2.ui',
3559                         method: 'crypt',
3560                         params: [ 'data' ],
3561                         expect: { crypt: '' }
3562                 }),
3563
3564
3565                 _acl_merge_scope: function(acl_scope, scope)
3566                 {
3567                         if ($.isArray(scope))
3568                         {
3569                                 for (var i = 0; i < scope.length; i++)
3570                                         acl_scope[scope[i]] = true;
3571                         }
3572                         else if ($.isPlainObject(scope))
3573                         {
3574                                 for (var object_name in scope)
3575                                 {
3576                                         if (!$.isArray(scope[object_name]))
3577                                                 continue;
3578
3579                                         var acl_object = acl_scope[object_name] || (acl_scope[object_name] = { });
3580
3581                                         for (var i = 0; i < scope[object_name].length; i++)
3582                                                 acl_object[scope[object_name][i]] = true;
3583                                 }
3584                         }
3585                 },
3586
3587                 _acl_merge_permission: function(acl_perm, perm)
3588                 {
3589                         if ($.isPlainObject(perm))
3590                         {
3591                                 for (var scope_name in perm)
3592                                 {
3593                                         var acl_scope = acl_perm[scope_name] || (acl_perm[scope_name] = { });
3594                                         this._acl_merge_scope(acl_scope, perm[scope_name]);
3595                                 }
3596                         }
3597                 },
3598
3599                 _acl_merge_group: function(acl_group, group)
3600                 {
3601                         if ($.isPlainObject(group))
3602                         {
3603                                 if (!acl_group.description)
3604                                         acl_group.description = group.description;
3605
3606                                 if (group.read)
3607                                 {
3608                                         var acl_perm = acl_group.read || (acl_group.read = { });
3609                                         this._acl_merge_permission(acl_perm, group.read);
3610                                 }
3611
3612                                 if (group.write)
3613                                 {
3614                                         var acl_perm = acl_group.write || (acl_group.write = { });
3615                                         this._acl_merge_permission(acl_perm, group.write);
3616                                 }
3617                         }
3618                 },
3619
3620                 _acl_merge_tree: function(acl_tree, tree)
3621                 {
3622                         if ($.isPlainObject(tree))
3623                         {
3624                                 for (var group_name in tree)
3625                                 {
3626                                         var acl_group = acl_tree[group_name] || (acl_tree[group_name] = { });
3627                                         this._acl_merge_group(acl_group, tree[group_name]);
3628                                 }
3629                         }
3630                 },
3631
3632                 listAvailableACLs: L.rpc.declare({
3633                         object: 'luci2.ui',
3634                         method: 'acls',
3635                         expect: { acls: [ ] },
3636                         filter: function(trees) {
3637                                 var acl_tree = { };
3638                                 for (var i = 0; i < trees.length; i++)
3639                                         L.ui._acl_merge_tree(acl_tree, trees[i]);
3640                                 return acl_tree;
3641                         }
3642                 }),
3643
3644                 _render_change_indicator: function()
3645                 {
3646                         return $('<ul />')
3647                                 .addClass('nav navbar-nav navbar-right')
3648                                 .append($('<li />')
3649                                         .append($('<a />')
3650                                                 .attr('id', 'changes')
3651                                                 .attr('href', '#')
3652                                                 .append($('<span />')
3653                                                         .addClass('label label-info'))));
3654                 },
3655
3656                 renderMainMenu: L.rpc.declare({
3657                         object: 'luci2.ui',
3658                         method: 'menu',
3659                         expect: { menu: { } },
3660                         filter: function(entries) {
3661                                 L.globals.mainMenu = new L.ui.menu();
3662                                 L.globals.mainMenu.entries(entries);
3663
3664                                 $('#mainmenu')
3665                                         .empty()
3666                                         .append(L.globals.mainMenu.render(0, 1))
3667                                         .append(L.ui._render_change_indicator());
3668                         }
3669                 }),
3670
3671                 renderViewMenu: function()
3672                 {
3673                         $('#viewmenu')
3674                                 .empty()
3675                                 .append(L.globals.mainMenu.render(2, 900));
3676                 },
3677
3678                 renderView: function()
3679                 {
3680                         var node  = arguments[0];
3681                         var name  = node.view.split(/\//).join('.');
3682                         var cname = L.toClassName(name);
3683                         var views = L.views || (L.views = { });
3684                         var args  = [ ];
3685
3686                         for (var i = 1; i < arguments.length; i++)
3687                                 args.push(arguments[i]);
3688
3689                         if (L.globals.currentView)
3690                                 L.globals.currentView.finish();
3691
3692                         L.ui.renderViewMenu();
3693                         L.setHash('view', node.view);
3694
3695                         if (views[cname] instanceof L.ui.view)
3696                         {
3697                                 L.globals.currentView = views[cname];
3698                                 return views[cname].render.apply(views[cname], args);
3699                         }
3700
3701                         var url = L.globals.resource + '/view/' + name + '.js';
3702
3703                         return $.ajax(url, {
3704                                 method: 'GET',
3705                                 cache: true,
3706                                 dataType: 'text'
3707                         }).then(function(data) {
3708                                 try {
3709                                         var viewConstructorSource = (
3710                                                 '(function(L, $) { ' +
3711                                                         'return %s' +
3712                                                 '})(L, $);\n\n' +
3713                                                 '//@ sourceURL=%s'
3714                                         ).format(data, url);
3715
3716                                         var viewConstructor = eval(viewConstructorSource);
3717
3718                                         views[cname] = new viewConstructor({
3719                                                 name: name,
3720                                                 acls: node.write || { }
3721                                         });
3722
3723                                         L.globals.currentView = views[cname];
3724                                         return views[cname].render.apply(views[cname], args);
3725                                 }
3726                                 catch(e) {
3727                                         alert('Unable to instantiate view "%s": %s'.format(url, e));
3728                                 };
3729
3730                                 return $.Deferred().resolve();
3731                         });
3732                 },
3733
3734                 changeView: function()
3735                 {
3736                         var name = L.getHash('view');
3737                         var node = L.globals.defaultNode;
3738
3739                         if (name && L.globals.mainMenu)
3740                                 node = L.globals.mainMenu.getNode(name);
3741
3742                         if (node)
3743                         {
3744                                 L.ui.loading(true);
3745                                 L.ui.renderView(node).then(function() {
3746                                         L.ui.loading(false);
3747                                 });
3748                         }
3749                 },
3750
3751                 updateHostname: function()
3752                 {
3753                         return L.system.getBoardInfo().then(function(info) {
3754                                 if (info.hostname)
3755                                         $('#hostname').text(info.hostname);
3756                         });
3757                 },
3758
3759                 updateChanges: function()
3760                 {
3761                         return L.uci.changes().then(function(changes) {
3762                                 var n = 0;
3763                                 var html = '';
3764
3765                                 for (var config in changes)
3766                                 {
3767                                         var log = [ ];
3768
3769                                         for (var i = 0; i < changes[config].length; i++)
3770                                         {
3771                                                 var c = changes[config][i];
3772
3773                                                 switch (c[0])
3774                                                 {
3775                                                 case 'order':
3776                                                         log.push('uci reorder %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
3777                                                         break;
3778
3779                                                 case 'remove':
3780                                                         if (c.length < 3)
3781                                                                 log.push('uci delete %s.<del>%s</del>'.format(config, c[1]));
3782                                                         else
3783                                                                 log.push('uci delete %s.%s.<del>%s</del>'.format(config, c[1], c[2]));
3784                                                         break;
3785
3786                                                 case 'rename':
3787                                                         if (c.length < 4)
3788                                                                 log.push('uci rename %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3]));
3789                                                         else
3790                                                                 log.push('uci rename %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
3791                                                         break;
3792
3793                                                 case 'add':
3794                                                         log.push('uci add %s <ins>%s</ins> (= <ins><strong>%s</strong></ins>)'.format(config, c[2], c[1]));
3795                                                         break;
3796
3797                                                 case 'list-add':
3798                                                         log.push('uci add_list %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
3799                                                         break;
3800
3801                                                 case 'list-del':
3802                                                         log.push('uci del_list %s.%s.<del>%s=<strong>%s</strong></del>'.format(config, c[1], c[2], c[3], c[4]));
3803                                                         break;
3804
3805                                                 case 'set':
3806                                                         if (c.length < 4)
3807                                                                 log.push('uci set %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
3808                                                         else
3809                                                                 log.push('uci set %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
3810                                                         break;
3811                                                 }
3812                                         }
3813
3814                                         html += '<code>/etc/config/%s</code><pre class="uci-changes">%s</pre>'.format(config, log.join('\n'));
3815                                         n += changes[config].length;
3816                                 }
3817
3818                                 if (n > 0)
3819                                         $('#changes')
3820                                                 .click(function(ev) {
3821                                                         L.ui.dialog(L.tr('Staged configuration changes'), html, {
3822                                                                 style: 'confirm',
3823                                                                 confirm: function() {
3824                                                                         L.uci.apply().then(
3825                                                                                 function(code) { alert('Success with code ' + code); },
3826                                                                                 function(code) { alert('Error with code ' + code); }
3827                                                                         );
3828                                                                 }
3829                                                         });
3830                                                         ev.preventDefault();
3831                                                 })
3832                                                 .children('span')
3833                                                         .show()
3834                                                         .text(L.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n));
3835                                 else
3836                                         $('#changes').children('span').hide();
3837                         });
3838                 },
3839
3840                 init: function()
3841                 {
3842                         L.ui.loading(true);
3843
3844                         $.when(
3845                                 L.session.updateACLs(),
3846                                 L.ui.updateHostname(),
3847                                 L.ui.updateChanges(),
3848                                 L.ui.renderMainMenu(),
3849                                 L.NetworkModel.init()
3850                         ).then(function() {
3851                                 L.ui.renderView(L.globals.defaultNode).then(function() {
3852                                         L.ui.loading(false);
3853                                 });
3854
3855                                 $(window).on('hashchange', function() {
3856                                         L.ui.changeView();
3857                                 });
3858                         });
3859                 },
3860
3861                 button: function(label, style, title)
3862                 {
3863                         style = style || 'default';
3864
3865                         return $('<button />')
3866                                 .attr('type', 'button')
3867                                 .attr('title', title ? title : '')
3868                                 .addClass('btn btn-' + style)
3869                                 .text(label);
3870                 }
3871         };
3872
3873         this.ui.AbstractWidget = Class.extend({
3874                 i18n: function(text) {
3875                         return text;
3876                 },
3877
3878                 label: function() {
3879                         var key = arguments[0];
3880                         var args = [ ];
3881
3882                         for (var i = 1; i < arguments.length; i++)
3883                                 args.push(arguments[i]);
3884
3885                         switch (typeof(this.options[key]))
3886                         {
3887                         case 'undefined':
3888                                 return '';
3889
3890                         case 'function':
3891                                 return this.options[key].apply(this, args);
3892
3893                         default:
3894                                 return ''.format.apply('' + this.options[key], args);
3895                         }
3896                 },
3897
3898                 toString: function() {
3899                         return $('<div />').append(this.render()).html();
3900                 },
3901
3902                 insertInto: function(id) {
3903                         return $(id).empty().append(this.render());
3904                 },
3905
3906                 appendTo: function(id) {
3907                         return $(id).append(this.render());
3908                 },
3909
3910                 on: function(evname, evfunc)
3911                 {
3912                         var evnames = L.toArray(evname);
3913
3914                         if (!this.events)
3915                                 this.events = { };
3916
3917                         for (var i = 0; i < evnames.length; i++)
3918                                 this.events[evnames[i]] = evfunc;
3919
3920                         return this;
3921                 },
3922
3923                 trigger: function(evname, evdata)
3924                 {
3925                         if (this.events)
3926                         {
3927                                 var evnames = L.toArray(evname);
3928
3929                                 for (var i = 0; i < evnames.length; i++)
3930                                         if (this.events[evnames[i]])
3931                                                 this.events[evnames[i]].call(this, evdata);
3932                         }
3933
3934                         return this;
3935                 }
3936         });
3937
3938         this.ui.view = this.ui.AbstractWidget.extend({
3939                 _fetch_template: function()
3940                 {
3941                         return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', {
3942                                 method: 'GET',
3943                                 cache: true,
3944                                 dataType: 'text',
3945                                 success: function(data) {
3946                                         data = data.replace(/<%([#:=])?(.+?)%>/g, function(match, p1, p2) {
3947                                                 p2 = p2.replace(/^\s+/, '').replace(/\s+$/, '');
3948                                                 switch (p1)
3949                                                 {
3950                                                 case '#':
3951                                                         return '';
3952
3953                                                 case ':':
3954                                                         return L.tr(p2);
3955
3956                                                 case '=':
3957                                                         return L.globals[p2] || '';
3958
3959                                                 default:
3960                                                         return '(?' + match + ')';
3961                                                 }
3962                                         });
3963
3964                                         $('#maincontent').append(data);
3965                                 }
3966                         });
3967                 },
3968
3969                 execute: function()
3970                 {
3971                         throw "Not implemented";
3972                 },
3973
3974                 render: function()
3975                 {
3976                         var container = $('#maincontent');
3977
3978                         container.empty();
3979
3980                         if (this.title)
3981                                 container.append($('<h2 />').append(this.title));
3982
3983                         if (this.description)
3984                                 container.append($('<p />').append(this.description));
3985
3986                         var self = this;
3987                         var args = [ ];
3988
3989                         for (var i = 0; i < arguments.length; i++)
3990                                 args.push(arguments[i]);
3991
3992                         return this._fetch_template().then(function() {
3993                                 return L.deferrable(self.execute.apply(self, args));
3994                         });
3995                 },
3996
3997                 repeat: function(func, interval)
3998                 {
3999                         var self = this;
4000
4001                         if (!self._timeouts)
4002                                 self._timeouts = [ ];
4003
4004                         var index = self._timeouts.length;
4005
4006                         if (typeof(interval) != 'number')
4007                                 interval = 5000;
4008
4009                         var setTimer, runTimer;
4010
4011                         setTimer = function() {
4012                                 if (self._timeouts)
4013                                         self._timeouts[index] = window.setTimeout(runTimer, interval);
4014                         };
4015
4016                         runTimer = function() {
4017                                 L.deferrable(func.call(self)).then(setTimer, setTimer);
4018                         };
4019
4020                         runTimer();
4021                 },
4022
4023                 finish: function()
4024                 {
4025                         if ($.isArray(this._timeouts))
4026                         {
4027                                 for (var i = 0; i < this._timeouts.length; i++)
4028                                         window.clearTimeout(this._timeouts[i]);
4029
4030                                 delete this._timeouts;
4031                         }
4032                 }
4033         });
4034
4035         this.ui.menu = this.ui.AbstractWidget.extend({
4036                 init: function() {
4037                         this._nodes = { };
4038                 },
4039
4040                 entries: function(entries)
4041                 {
4042                         for (var entry in entries)
4043                         {
4044                                 var path = entry.split(/\//);
4045                                 var node = this._nodes;
4046
4047                                 for (i = 0; i < path.length; i++)
4048                                 {
4049                                         if (!node.childs)
4050                                                 node.childs = { };
4051
4052                                         if (!node.childs[path[i]])
4053                                                 node.childs[path[i]] = { };
4054
4055                                         node = node.childs[path[i]];
4056                                 }
4057
4058                                 $.extend(node, entries[entry]);
4059                         }
4060                 },
4061
4062                 _indexcmp: function(a, b)
4063                 {
4064                         var x = a.index || 0;
4065                         var y = b.index || 0;
4066                         return (x - y);
4067                 },
4068
4069                 firstChildView: function(node)
4070                 {
4071                         if (node.view)
4072                                 return node;
4073
4074                         var nodes = [ ];
4075                         for (var child in (node.childs || { }))
4076                                 nodes.push(node.childs[child]);
4077
4078                         nodes.sort(this._indexcmp);
4079
4080                         for (var i = 0; i < nodes.length; i++)
4081                         {
4082                                 var child = this.firstChildView(nodes[i]);
4083                                 if (child)
4084                                 {
4085                                         for (var key in child)
4086                                                 if (!node.hasOwnProperty(key) && child.hasOwnProperty(key))
4087                                                         node[key] = child[key];
4088
4089                                         return node;
4090                                 }
4091                         }
4092
4093                         return undefined;
4094                 },
4095
4096                 _onclick: function(ev)
4097                 {
4098                         L.setHash('view', ev.data);
4099
4100                         ev.preventDefault();
4101                         this.blur();
4102                 },
4103
4104                 _render: function(childs, level, min, max)
4105                 {
4106                         var nodes = [ ];
4107                         for (var node in childs)
4108                         {
4109                                 var child = this.firstChildView(childs[node]);
4110                                 if (child)
4111                                         nodes.push(childs[node]);
4112                         }
4113
4114                         nodes.sort(this._indexcmp);
4115
4116                         var list = $('<ul />');
4117
4118                         if (level == 0)
4119                                 list.addClass('nav').addClass('navbar-nav');
4120                         else if (level == 1)
4121                                 list.addClass('dropdown-menu').addClass('navbar-inverse');
4122
4123                         for (var i = 0; i < nodes.length; i++)
4124                         {
4125                                 if (!L.globals.defaultNode)
4126                                 {
4127                                         var v = L.getHash('view');
4128                                         if (!v || v == nodes[i].view)
4129                                                 L.globals.defaultNode = nodes[i];
4130                                 }
4131
4132                                 var item = $('<li />')
4133                                         .append($('<a />')
4134                                                 .attr('href', '#')
4135                                                 .text(L.tr(nodes[i].title)))
4136                                         .appendTo(list);
4137
4138                                 if (nodes[i].childs && level < max)
4139                                 {
4140                                         item.addClass('dropdown');
4141
4142                                         item.find('a')
4143                                                 .addClass('dropdown-toggle')
4144                                                 .attr('data-toggle', 'dropdown')
4145                                                 .append('<b class="caret"></b>');
4146
4147                                         item.append(this._render(nodes[i].childs, level + 1));
4148                                 }
4149                                 else
4150                                 {
4151                                         item.find('a').click(nodes[i].view, this._onclick);
4152                                 }
4153                         }
4154
4155                         return list.get(0);
4156                 },
4157
4158                 render: function(min, max)
4159                 {
4160                         var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes;
4161                         return this._render(top.childs, 0, min, max);
4162                 },
4163
4164                 getNode: function(path, max)
4165                 {
4166                         var p = path.split(/\//);
4167                         var n = this._nodes;
4168
4169                         if (typeof(max) == 'undefined')
4170                                 max = p.length;
4171
4172                         for (var i = 0; i < max; i++)
4173                         {
4174                                 if (!n.childs[p[i]])
4175                                         return undefined;
4176
4177                                 n = n.childs[p[i]];
4178                         }
4179
4180                         return n;
4181                 }
4182         });
4183
4184         this.ui.table = this.ui.AbstractWidget.extend({
4185                 init: function()
4186                 {
4187                         this._rows = [ ];
4188                 },
4189
4190                 row: function(values)
4191                 {
4192                         if ($.isArray(values))
4193                         {
4194                                 this._rows.push(values);
4195                         }
4196                         else if ($.isPlainObject(values))
4197                         {
4198                                 var v = [ ];
4199                                 for (var i = 0; i < this.options.columns.length; i++)
4200                                 {
4201                                         var col = this.options.columns[i];
4202
4203                                         if (typeof col.key == 'string')
4204                                                 v.push(values[col.key]);
4205                                         else
4206                                                 v.push(null);
4207                                 }
4208                                 this._rows.push(v);
4209                         }
4210                 },
4211
4212                 rows: function(rows)
4213                 {
4214                         for (var i = 0; i < rows.length; i++)
4215                                 this.row(rows[i]);
4216                 },
4217
4218                 render: function(id)
4219                 {
4220                         var fieldset = document.createElement('fieldset');
4221                                 fieldset.className = 'cbi-section';
4222
4223                         if (this.options.caption)
4224                         {
4225                                 var legend = document.createElement('legend');
4226                                 $(legend).append(this.options.caption);
4227                                 fieldset.appendChild(legend);
4228                         }
4229
4230                         var table = document.createElement('table');
4231                                 table.className = 'table table-condensed table-hover';
4232
4233                         var has_caption = false;
4234                         var has_description = false;
4235
4236                         for (var i = 0; i < this.options.columns.length; i++)
4237                                 if (this.options.columns[i].caption)
4238                                 {
4239                                         has_caption = true;
4240                                         break;
4241                                 }
4242                                 else if (this.options.columns[i].description)
4243                                 {
4244                                         has_description = true;
4245                                         break;
4246                                 }
4247
4248                         if (has_caption)
4249                         {
4250                                 var tr = table.insertRow(-1);
4251                                         tr.className = 'cbi-section-table-titles';
4252
4253                                 for (var i = 0; i < this.options.columns.length; i++)
4254                                 {
4255                                         var col = this.options.columns[i];
4256                                         var th = document.createElement('th');
4257                                                 th.className = 'cbi-section-table-cell';
4258
4259                                         tr.appendChild(th);
4260
4261                                         if (col.width)
4262                                                 th.style.width = col.width;
4263
4264                                         if (col.align)
4265                                                 th.style.textAlign = col.align;
4266
4267                                         if (col.caption)
4268                                                 $(th).append(col.caption);
4269                                 }
4270                         }
4271
4272                         if (has_description)
4273                         {
4274                                 var tr = table.insertRow(-1);
4275                                         tr.className = 'cbi-section-table-descr';
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.description)
4292                                                 $(th).append(col.description);
4293                                 }
4294                         }
4295
4296                         if (this._rows.length == 0)
4297                         {
4298                                 if (this.options.placeholder)
4299                                 {
4300                                         var tr = table.insertRow(-1);
4301                                         var td = tr.insertCell(-1);
4302                                                 td.className = 'cbi-section-table-cell';
4303
4304                                         td.colSpan = this.options.columns.length;
4305                                         $(td).append(this.options.placeholder);
4306                                 }
4307                         }
4308                         else
4309                         {
4310                                 for (var i = 0; i < this._rows.length; i++)
4311                                 {
4312                                         var tr = table.insertRow(-1);
4313
4314                                         for (var j = 0; j < this.options.columns.length; j++)
4315                                         {
4316                                                 var col = this.options.columns[j];
4317                                                 var td = tr.insertCell(-1);
4318
4319                                                 var val = this._rows[i][j];
4320
4321                                                 if (typeof(val) == 'undefined')
4322                                                         val = col.placeholder;
4323
4324                                                 if (typeof(val) == 'undefined')
4325                                                         val = '';
4326
4327                                                 if (col.width)
4328                                                         td.style.width = col.width;
4329
4330                                                 if (col.align)
4331                                                         td.style.textAlign = col.align;
4332
4333                                                 if (typeof col.format == 'string')
4334                                                         $(td).append(col.format.format(val));
4335                                                 else if (typeof col.format == 'function')
4336                                                         $(td).append(col.format(val, i));
4337                                                 else
4338                                                         $(td).append(val);
4339                                         }
4340                                 }
4341                         }
4342
4343                         this._rows = [ ];
4344                         fieldset.appendChild(table);
4345
4346                         return fieldset;
4347                 }
4348         });
4349
4350         this.ui.progress = this.ui.AbstractWidget.extend({
4351                 render: function()
4352                 {
4353                         var vn = parseInt(this.options.value) || 0;
4354                         var mn = parseInt(this.options.max) || 100;
4355                         var pc = Math.floor((100 / mn) * vn);
4356
4357                         var text;
4358
4359                         if (typeof(this.options.format) == 'string')
4360                                 text = this.options.format.format(this.options.value, this.options.max, pc);
4361                         else if (typeof(this.options.format) == 'function')
4362                                 text = this.options.format(pc);
4363                         else
4364                                 text = '%.2f%%'.format(pc);
4365
4366                         return $('<div />')
4367                                 .addClass('progress')
4368                                 .append($('<div />')
4369                                         .addClass('progress-bar')
4370                                         .addClass('progress-bar-info')
4371                                         .css('width', pc + '%'))
4372                                 .append($('<small />')
4373                                         .text(text));
4374                 }
4375         });
4376
4377         this.ui.devicebadge = this.ui.AbstractWidget.extend({
4378                 render: function()
4379                 {
4380                         var l2dev = this.options.l2_device || this.options.device;
4381                         var l3dev = this.options.l3_device;
4382                         var dev = l3dev || l2dev || '?';
4383
4384                         var span = document.createElement('span');
4385                                 span.className = 'badge';
4386
4387                         if (typeof(this.options.signal) == 'number' ||
4388                                 typeof(this.options.noise) == 'number')
4389                         {
4390                                 var r = 'none';
4391                                 if (typeof(this.options.signal) != 'undefined' &&
4392                                         typeof(this.options.noise) != 'undefined')
4393                                 {
4394                                         var q = (-1 * (this.options.noise - this.options.signal)) / 5;
4395                                         if (q < 1)
4396                                                 r = '0';
4397                                         else if (q < 2)
4398                                                 r = '0-25';
4399                                         else if (q < 3)
4400                                                 r = '25-50';
4401                                         else if (q < 4)
4402                                                 r = '50-75';
4403                                         else
4404                                                 r = '75-100';
4405                                 }
4406
4407                                 span.appendChild(document.createElement('img'));
4408                                 span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png';
4409
4410                                 if (r == 'none')
4411                                         span.title = L.tr('No signal');
4412                                 else
4413                                         span.title = '%s: %d %s / %s: %d %s'.format(
4414                                                 L.tr('Signal'), this.options.signal, L.tr('dBm'),
4415                                                 L.tr('Noise'), this.options.noise, L.tr('dBm')
4416                                         );
4417                         }
4418                         else
4419                         {
4420                                 var type = 'ethernet';
4421                                 var desc = L.tr('Ethernet device');
4422
4423                                 if (l3dev != l2dev)
4424                                 {
4425                                         type = 'tunnel';
4426                                         desc = L.tr('Tunnel interface');
4427                                 }
4428                                 else if (dev.indexOf('br-') == 0)
4429                                 {
4430                                         type = 'bridge';
4431                                         desc = L.tr('Bridge');
4432                                 }
4433                                 else if (dev.indexOf('.') > 0)
4434                                 {
4435                                         type = 'vlan';
4436                                         desc = L.tr('VLAN interface');
4437                                 }
4438                                 else if (dev.indexOf('wlan') == 0 ||
4439                                                  dev.indexOf('ath') == 0 ||
4440                                                  dev.indexOf('wl') == 0)
4441                                 {
4442                                         type = 'wifi';
4443                                         desc = L.tr('Wireless Network');
4444                                 }
4445
4446                                 span.appendChild(document.createElement('img'));
4447                                 span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
4448                                 span.title = desc;
4449                         }
4450
4451                         $(span).append(' ');
4452                         $(span).append(dev);
4453
4454                         return span;
4455                 }
4456         });
4457
4458         var type = function(f, l)
4459         {
4460                 f.message = l;
4461                 return f;
4462         };
4463
4464         this.cbi = {
4465                 validation: {
4466                         i18n: function(msg)
4467                         {
4468                                 L.cbi.validation.message = L.tr(msg);
4469                         },
4470
4471                         compile: function(code)
4472                         {
4473                                 var pos = 0;
4474                                 var esc = false;
4475                                 var depth = 0;
4476                                 var types = L.cbi.validation.types;
4477                                 var stack = [ ];
4478
4479                                 code += ',';
4480
4481                                 for (var i = 0; i < code.length; i++)
4482                                 {
4483                                         if (esc)
4484                                         {
4485                                                 esc = false;
4486                                                 continue;
4487                                         }
4488
4489                                         switch (code.charCodeAt(i))
4490                                         {
4491                                         case 92:
4492                                                 esc = true;
4493                                                 break;
4494
4495                                         case 40:
4496                                         case 44:
4497                                                 if (depth <= 0)
4498                                                 {
4499                                                         if (pos < i)
4500                                                         {
4501                                                                 var label = code.substring(pos, i);
4502                                                                         label = label.replace(/\\(.)/g, '$1');
4503                                                                         label = label.replace(/^[ \t]+/g, '');
4504                                                                         label = label.replace(/[ \t]+$/g, '');
4505
4506                                                                 if (label && !isNaN(label))
4507                                                                 {
4508                                                                         stack.push(parseFloat(label));
4509                                                                 }
4510                                                                 else if (label.match(/^(['"]).*\1$/))
4511                                                                 {
4512                                                                         stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
4513                                                                 }
4514                                                                 else if (typeof types[label] == 'function')
4515                                                                 {
4516                                                                         stack.push(types[label]);
4517                                                                         stack.push([ ]);
4518                                                                 }
4519                                                                 else
4520                                                                 {
4521                                                                         throw "Syntax error, unhandled token '"+label+"'";
4522                                                                 }
4523                                                         }
4524                                                         pos = i+1;
4525                                                 }
4526                                                 depth += (code.charCodeAt(i) == 40);
4527                                                 break;
4528
4529                                         case 41:
4530                                                 if (--depth <= 0)
4531                                                 {
4532                                                         if (typeof stack[stack.length-2] != 'function')
4533                                                                 throw "Syntax error, argument list follows non-function";
4534
4535                                                         stack[stack.length-1] =
4536                                                                 L.cbi.validation.compile(code.substring(pos, i));
4537
4538                                                         pos = i+1;
4539                                                 }
4540                                                 break;
4541                                         }
4542                                 }
4543
4544                                 return stack;
4545                         }
4546                 }
4547         };
4548
4549         var validation = this.cbi.validation;
4550
4551         validation.types = {
4552                 'integer': function()
4553                 {
4554                         if (this.match(/^-?[0-9]+$/) != null)
4555                                 return true;
4556
4557                         validation.i18n('Must be a valid integer');
4558                         return false;
4559                 },
4560
4561                 'uinteger': function()
4562                 {
4563                         if (validation.types['integer'].apply(this) && (this >= 0))
4564                                 return true;
4565
4566                         validation.i18n('Must be a positive integer');
4567                         return false;
4568                 },
4569
4570                 'float': function()
4571                 {
4572                         if (!isNaN(parseFloat(this)))
4573                                 return true;
4574
4575                         validation.i18n('Must be a valid number');
4576                         return false;
4577                 },
4578
4579                 'ufloat': function()
4580                 {
4581                         if (validation.types['float'].apply(this) && (this >= 0))
4582                                 return true;
4583
4584                         validation.i18n('Must be a positive number');
4585                         return false;
4586                 },
4587
4588                 'ipaddr': function()
4589                 {
4590                         if (validation.types['ip4addr'].apply(this) ||
4591                                 validation.types['ip6addr'].apply(this))
4592                                 return true;
4593
4594                         validation.i18n('Must be a valid IP address');
4595                         return false;
4596                 },
4597
4598                 'ip4addr': function()
4599                 {
4600                         if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/))
4601                         {
4602                                 if ((RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
4603                                     (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
4604                                     (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
4605                                     (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
4606                                     ((RegExp.$6.indexOf('.') < 0)
4607                                       ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32))
4608                                       : (validation.types['ip4addr'].apply(RegExp.$6))))
4609                                         return true;
4610                         }
4611
4612                         validation.i18n('Must be a valid IPv4 address');
4613                         return false;
4614                 },
4615
4616                 'ip6addr': function()
4617                 {
4618                         if (this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/))
4619                         {
4620                                 if (!RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)))
4621                                 {
4622                                         var addr = RegExp.$1;
4623
4624                                         if (addr == '::')
4625                                         {
4626                                                 return true;
4627                                         }
4628
4629                                         if (addr.indexOf('.') > 0)
4630                                         {
4631                                                 var off = addr.lastIndexOf(':');
4632
4633                                                 if (!(off && validation.types['ip4addr'].apply(addr.substr(off+1))))
4634                                                 {
4635                                                         validation.i18n('Must be a valid IPv6 address');
4636                                                         return false;
4637                                                 }
4638
4639                                                 addr = addr.substr(0, off) + ':0:0';
4640                                         }
4641
4642                                         if (addr.indexOf('::') >= 0)
4643                                         {
4644                                                 var colons = 0;
4645                                                 var fill = '0';
4646
4647                                                 for (var i = 1; i < (addr.length-1); i++)
4648                                                         if (addr.charAt(i) == ':')
4649                                                                 colons++;
4650
4651                                                 if (colons > 7)
4652                                                 {
4653                                                         validation.i18n('Must be a valid IPv6 address');
4654                                                         return false;
4655                                                 }
4656
4657                                                 for (var i = 0; i < (7 - colons); i++)
4658                                                         fill += ':0';
4659
4660                                                 if (addr.match(/^(.*?)::(.*?)$/))
4661                                                         addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
4662                                                                    (RegExp.$2 ? ':' + RegExp.$2 : '');
4663                                         }
4664
4665                                         if (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null)
4666                                                 return true;
4667
4668                                         validation.i18n('Must be a valid IPv6 address');
4669                                         return false;
4670                                 }
4671                         }
4672
4673                         validation.i18n('Must be a valid IPv6 address');
4674                         return false;
4675                 },
4676
4677                 'port': function()
4678                 {
4679                         if (validation.types['integer'].apply(this) &&
4680                                 (this >= 0) && (this <= 65535))
4681                                 return true;
4682
4683                         validation.i18n('Must be a valid port number');
4684                         return false;
4685                 },
4686
4687                 'portrange': function()
4688                 {
4689                         if (this.match(/^(\d+)-(\d+)$/))
4690                         {
4691                                 var p1 = RegExp.$1;
4692                                 var p2 = RegExp.$2;
4693
4694                                 if (validation.types['port'].apply(p1) &&
4695                                     validation.types['port'].apply(p2) &&
4696                                     (parseInt(p1) <= parseInt(p2)))
4697                                         return true;
4698                         }
4699                         else if (validation.types['port'].apply(this))
4700                         {
4701                                 return true;
4702                         }
4703
4704                         validation.i18n('Must be a valid port range');
4705                         return false;
4706                 },
4707
4708                 'macaddr': function()
4709                 {
4710                         if (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null)
4711                                 return true;
4712
4713                         validation.i18n('Must be a valid MAC address');
4714                         return false;
4715                 },
4716
4717                 'host': function()
4718                 {
4719                         if (validation.types['hostname'].apply(this) ||
4720                             validation.types['ipaddr'].apply(this))
4721                                 return true;
4722
4723                         validation.i18n('Must be a valid hostname or IP address');
4724                         return false;
4725                 },
4726
4727                 'hostname': function()
4728                 {
4729                         if ((this.length <= 253) &&
4730                             ((this.match(/^[a-zA-Z0-9]+$/) != null ||
4731                              (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
4732                               this.match(/[^0-9.]/)))))
4733                                 return true;
4734
4735                         validation.i18n('Must be a valid host name');
4736                         return false;
4737                 },
4738
4739                 'network': function()
4740                 {
4741                         if (validation.types['uciname'].apply(this) ||
4742                             validation.types['host'].apply(this))
4743                                 return true;
4744
4745                         validation.i18n('Must be a valid network name');
4746                         return false;
4747                 },
4748
4749                 'wpakey': function()
4750                 {
4751                         var v = this;
4752
4753                         if ((v.length == 64)
4754                               ? (v.match(/^[a-fA-F0-9]{64}$/) != null)
4755                                   : ((v.length >= 8) && (v.length <= 63)))
4756                                 return true;
4757
4758                         validation.i18n('Must be a valid WPA key');
4759                         return false;
4760                 },
4761
4762                 'wepkey': function()
4763                 {
4764                         var v = this;
4765
4766                         if (v.substr(0,2) == 's:')
4767                                 v = v.substr(2);
4768
4769                         if (((v.length == 10) || (v.length == 26))
4770                               ? (v.match(/^[a-fA-F0-9]{10,26}$/) != null)
4771                               : ((v.length == 5) || (v.length == 13)))
4772                                 return true;
4773
4774                         validation.i18n('Must be a valid WEP key');
4775                         return false;
4776                 },
4777
4778                 'uciname': function()
4779                 {
4780                         if (this.match(/^[a-zA-Z0-9_]+$/) != null)
4781                                 return true;
4782
4783                         validation.i18n('Must be a valid UCI identifier');
4784                         return false;
4785                 },
4786
4787                 'range': function(min, max)
4788                 {
4789                         var val = parseFloat(this);
4790
4791                         if (validation.types['integer'].apply(this) &&
4792                             !isNaN(min) && !isNaN(max) && ((val >= min) && (val <= max)))
4793                                 return true;
4794
4795                         validation.i18n('Must be a number between %d and %d');
4796                         return false;
4797                 },
4798
4799                 'min': function(min)
4800                 {
4801                         var val = parseFloat(this);
4802
4803                         if (validation.types['integer'].apply(this) &&
4804                             !isNaN(min) && !isNaN(val) && (val >= min))
4805                                 return true;
4806
4807                         validation.i18n('Must be a number greater or equal to %d');
4808                         return false;
4809                 },
4810
4811                 'max': function(max)
4812                 {
4813                         var val = parseFloat(this);
4814
4815                         if (validation.types['integer'].apply(this) &&
4816                             !isNaN(max) && !isNaN(val) && (val <= max))
4817                                 return true;
4818
4819                         validation.i18n('Must be a number lower or equal to %d');
4820                         return false;
4821                 },
4822
4823                 'rangelength': function(min, max)
4824                 {
4825                         var val = '' + this;
4826
4827                         if (!isNaN(min) && !isNaN(max) &&
4828                             (val.length >= min) && (val.length <= max))
4829                                 return true;
4830
4831                         validation.i18n('Must be between %d and %d characters');
4832                         return false;
4833                 },
4834
4835                 'minlength': function(min)
4836                 {
4837                         var val = '' + this;
4838
4839                         if (!isNaN(min) && (val.length >= min))
4840                                 return true;
4841
4842                         validation.i18n('Must be at least %d characters');
4843                         return false;
4844                 },
4845
4846                 'maxlength': function(max)
4847                 {
4848                         var val = '' + this;
4849
4850                         if (!isNaN(max) && (val.length <= max))
4851                                 return true;
4852
4853                         validation.i18n('Must be at most %d characters');
4854                         return false;
4855                 },
4856
4857                 'or': function()
4858                 {
4859                         var msgs = [ ];
4860
4861                         for (var i = 0; i < arguments.length; i += 2)
4862                         {
4863                                 delete validation.message;
4864
4865                                 if (typeof(arguments[i]) != 'function')
4866                                 {
4867                                         if (arguments[i] == this)
4868                                                 return true;
4869                                         i--;
4870                                 }
4871                                 else if (arguments[i].apply(this, arguments[i+1]))
4872                                 {
4873                                         return true;
4874                                 }
4875
4876                                 if (validation.message)
4877                                         msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
4878                         }
4879
4880                         validation.message = msgs.join( L.tr(' - or - '));
4881                         return false;
4882                 },
4883
4884                 'and': function()
4885                 {
4886                         var msgs = [ ];
4887
4888                         for (var i = 0; i < arguments.length; i += 2)
4889                         {
4890                                 delete validation.message;
4891
4892                                 if (typeof arguments[i] != 'function')
4893                                 {
4894                                         if (arguments[i] != this)
4895                                                 return false;
4896                                         i--;
4897                                 }
4898                                 else if (!arguments[i].apply(this, arguments[i+1]))
4899                                 {
4900                                         return false;
4901                                 }
4902
4903                                 if (validation.message)
4904                                         msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
4905                         }
4906
4907                         validation.message = msgs.join(', ');
4908                         return true;
4909                 },
4910
4911                 'neg': function()
4912                 {
4913                         return validation.types['or'].apply(
4914                                 this.replace(/^[ \t]*![ \t]*/, ''), arguments);
4915                 },
4916
4917                 'list': function(subvalidator, subargs)
4918                 {
4919                         if (typeof subvalidator != 'function')
4920                                 return false;
4921
4922                         var tokens = this.match(/[^ \t]+/g);
4923                         for (var i = 0; i < tokens.length; i++)
4924                                 if (!subvalidator.apply(tokens[i], subargs))
4925                                         return false;
4926
4927                         return true;
4928                 },
4929
4930                 'phonedigit': function()
4931                 {
4932                         if (this.match(/^[0-9\*#!\.]+$/) != null)
4933                                 return true;
4934
4935                         validation.i18n('Must be a valid phone number digit');
4936                         return false;
4937                 },
4938
4939                 'string': function()
4940                 {
4941                         return true;
4942                 }
4943         };
4944
4945
4946         this.cbi.AbstractValue = this.ui.AbstractWidget.extend({
4947                 init: function(name, options)
4948                 {
4949                         this.name = name;
4950                         this.instance = { };
4951                         this.dependencies = [ ];
4952                         this.rdependency = { };
4953
4954                         this.options = L.defaults(options, {
4955                                 placeholder: '',
4956                                 datatype: 'string',
4957                                 optional: false,
4958                                 keep: true
4959                         });
4960                 },
4961
4962                 id: function(sid)
4963                 {
4964                         return this.section.id('field', sid || '__unknown__', this.name);
4965                 },
4966
4967                 render: function(sid, condensed)
4968                 {
4969                         var i = this.instance[sid] = { };
4970
4971                         i.top = $('<div />');
4972
4973                         if (!condensed)
4974                         {
4975                                 i.top.addClass('form-group');
4976
4977                                 if (typeof(this.options.caption) == 'string')
4978                                         $('<label />')
4979                                                 .addClass('col-lg-2 control-label')
4980                                                 .attr('for', this.id(sid))
4981                                                 .text(this.options.caption)
4982                                                 .appendTo(i.top);
4983                         }
4984
4985                         i.error = $('<div />')
4986                                 .hide()
4987                                 .addClass('label label-danger');
4988
4989                         i.widget = $('<div />')
4990
4991                                 .append(this.widget(sid))
4992                                 .append(i.error)
4993                                 .appendTo(i.top);
4994
4995                         if (!condensed)
4996                         {
4997                                 i.widget.addClass('col-lg-5');
4998
4999                                 $('<div />')
5000                                         .addClass('col-lg-5')
5001                                         .text((typeof(this.options.description) == 'string') ? this.options.description : '')
5002                                         .appendTo(i.top);
5003                         }
5004
5005                         return i.top;
5006                 },
5007
5008                 active: function(sid)
5009                 {
5010                         return (this.instance[sid] && !this.instance[sid].disabled);
5011                 },
5012
5013                 ucipath: function(sid)
5014                 {
5015                         return {
5016                                 config:  (this.options.uci_package || this.map.uci_package),
5017                                 section: (this.options.uci_section || sid),
5018                                 option:  (this.options.uci_option  || this.name)
5019                         };
5020                 },
5021
5022                 ucivalue: function(sid)
5023                 {
5024                         var uci = this.ucipath(sid);
5025                         var val = this.map.get(uci.config, uci.section, uci.option);
5026
5027                         if (typeof(val) == 'undefined')
5028                                 return this.options.initial;
5029
5030                         return val;
5031                 },
5032
5033                 formvalue: function(sid)
5034                 {
5035                         var v = $('#' + this.id(sid)).val();
5036                         return (v === '') ? undefined : v;
5037                 },
5038
5039                 textvalue: function(sid)
5040                 {
5041                         var v = this.formvalue(sid);
5042
5043                         if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
5044                                 v = this.ucivalue(sid);
5045
5046                         if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
5047                                 v = this.options.placeholder;
5048
5049                         if (typeof(v) == 'undefined' || v === '')
5050                                 return undefined;
5051
5052                         if (typeof(v) == 'string' && $.isArray(this.choices))
5053                         {
5054                                 for (var i = 0; i < this.choices.length; i++)
5055                                         if (v === this.choices[i][0])
5056                                                 return this.choices[i][1];
5057                         }
5058                         else if (v === true)
5059                                 return L.tr('yes');
5060                         else if (v === false)
5061                                 return L.tr('no');
5062                         else if ($.isArray(v))
5063                                 return v.join(', ');
5064
5065                         return v;
5066                 },
5067
5068                 changed: function(sid)
5069                 {
5070                         var a = this.ucivalue(sid);
5071                         var b = this.formvalue(sid);
5072
5073                         if (typeof(a) != typeof(b))
5074                                 return true;
5075
5076                         if (typeof(a) == 'object')
5077                         {
5078                                 if (a.length != b.length)
5079                                         return true;
5080
5081                                 for (var i = 0; i < a.length; i++)
5082                                         if (a[i] != b[i])
5083                                                 return true;
5084
5085                                 return false;
5086                         }
5087
5088                         return (a != b);
5089                 },
5090
5091                 save: function(sid)
5092                 {
5093                         var uci = this.ucipath(sid);
5094
5095                         if (this.instance[sid].disabled)
5096                         {
5097                                 if (!this.options.keep)
5098                                         return this.map.set(uci.config, uci.section, uci.option, undefined);
5099
5100                                 return false;
5101                         }
5102
5103                         var chg = this.changed(sid);
5104                         var val = this.formvalue(sid);
5105
5106                         if (chg)
5107                                 this.map.set(uci.config, uci.section, uci.option, val);
5108
5109                         return chg;
5110                 },
5111
5112                 _ev_validate: function(ev)
5113                 {
5114                         var d = ev.data;
5115                         var rv = true;
5116                         var val = d.elem.val();
5117                         var vstack = d.vstack;
5118
5119                         if (vstack && typeof(vstack[0]) == 'function')
5120                         {
5121                                 delete validation.message;
5122
5123                                 if ((val.length == 0 && !d.opt))
5124                                 {
5125                                         d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
5126                                         d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
5127
5128                                         d.inst.error.text(L.tr('Field must not be empty')).show();
5129                                         rv = false;
5130                                 }
5131                                 else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
5132                                 {
5133                                         d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
5134                                         d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
5135
5136                                         d.inst.error.text(validation.message.format.apply(validation.message, vstack[1])).show();
5137                                         rv = false;
5138                                 }
5139                                 else
5140                                 {
5141                                         d.elem.parents('div.form-group, td').first().removeClass('luci2-form-error');
5142                                         d.elem.parents('div.input-group, div.form-group, td').first().removeClass('has-error');
5143
5144                                         if (d.multi && d.inst.widget && d.inst.widget.find('input.error, select.error').length > 0)
5145                                                 rv = false;
5146                                         else
5147                                                 d.inst.error.text('').hide();
5148                                 }
5149                         }
5150
5151                         if (rv)
5152                         {
5153                                 for (var field in d.self.rdependency)
5154                                         d.self.rdependency[field].toggle(d.sid);
5155
5156                                 d.self.section.tabtoggle(d.sid);
5157                         }
5158
5159                         return rv;
5160                 },
5161
5162                 validator: function(sid, elem, multi)
5163                 {
5164                         var evdata = {
5165                                 self:   this,
5166                                 sid:    sid,
5167                                 elem:   elem,
5168                                 multi:  multi,
5169                                 inst:   this.instance[sid],
5170                                 opt:    this.options.optional
5171                         };
5172
5173                         if (this.events)
5174                                 for (var evname in this.events)
5175                                         elem.on(evname, evdata, this.events[evname]);
5176
5177                         if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
5178                                 return elem;
5179
5180                         var vstack;
5181                         if (typeof(this.options.datatype) == 'string')
5182                         {
5183                                 try {
5184                                         evdata.vstack = L.cbi.validation.compile(this.options.datatype);
5185                                 } catch(e) { };
5186                         }
5187                         else if (typeof(this.options.datatype) == 'function')
5188                         {
5189                                 var vfunc = this.options.datatype;
5190                                 evdata.vstack = [ function(elem) {
5191                                         var rv = vfunc(this, elem);
5192                                         if (rv !== true)
5193                                                 validation.message = rv;
5194                                         return (rv === true);
5195                                 }, [ elem ] ];
5196                         }
5197
5198                         if (elem.prop('tagName') == 'SELECT')
5199                         {
5200                                 elem.change(evdata, this._ev_validate);
5201                         }
5202                         else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
5203                         {
5204                                 elem.click(evdata, this._ev_validate);
5205                                 elem.blur(evdata, this._ev_validate);
5206                         }
5207                         else
5208                         {
5209                                 elem.keyup(evdata, this._ev_validate);
5210                                 elem.blur(evdata, this._ev_validate);
5211                         }
5212
5213                         elem.attr('cbi-validate', true).on('validate', evdata, this._ev_validate);
5214
5215                         return elem;
5216                 },
5217
5218                 validate: function(sid)
5219                 {
5220                         var i = this.instance[sid];
5221
5222                         i.widget.find('[cbi-validate]').trigger('validate');
5223
5224                         return (i.disabled || i.error.text() == '');
5225                 },
5226
5227                 depends: function(d, v, add)
5228                 {
5229                         var dep;
5230
5231                         if ($.isArray(d))
5232                         {
5233                                 dep = { };
5234                                 for (var i = 0; i < d.length; i++)
5235                                 {
5236                                         if (typeof(d[i]) == 'string')
5237                                                 dep[d[i]] = true;
5238                                         else if (d[i] instanceof L.cbi.AbstractValue)
5239                                                 dep[d[i].name] = true;
5240                                 }
5241                         }
5242                         else if (d instanceof L.cbi.AbstractValue)
5243                         {
5244                                 dep = { };
5245                                 dep[d.name] = (typeof(v) == 'undefined') ? true : v;
5246                         }
5247                         else if (typeof(d) == 'object')
5248                         {
5249                                 dep = d;
5250                         }
5251                         else if (typeof(d) == 'string')
5252                         {
5253                                 dep = { };
5254                                 dep[d] = (typeof(v) == 'undefined') ? true : v;
5255                         }
5256
5257                         if (!dep || $.isEmptyObject(dep))
5258                                 return this;
5259
5260                         for (var field in dep)
5261                         {
5262                                 var f = this.section.fields[field];
5263                                 if (f)
5264                                         f.rdependency[this.name] = this;
5265                                 else
5266                                         delete dep[field];
5267                         }
5268
5269                         if ($.isEmptyObject(dep))
5270                                 return this;
5271
5272                         if (!add || !this.dependencies.length)
5273                                 this.dependencies.push(dep);
5274                         else
5275                                 for (var i = 0; i < this.dependencies.length; i++)
5276                                         $.extend(this.dependencies[i], dep);
5277
5278                         return this;
5279                 },
5280
5281                 toggle: function(sid)
5282                 {
5283                         var d = this.dependencies;
5284                         var i = this.instance[sid];
5285
5286                         if (!d.length)
5287                                 return true;
5288
5289                         for (var n = 0; n < d.length; n++)
5290                         {
5291                                 var rv = true;
5292
5293                                 for (var field in d[n])
5294                                 {
5295                                         var val = this.section.fields[field].formvalue(sid);
5296                                         var cmp = d[n][field];
5297
5298                                         if (typeof(cmp) == 'boolean')
5299                                         {
5300                                                 if (cmp == (typeof(val) == 'undefined' || val === '' || val === false))
5301                                                 {
5302                                                         rv = false;
5303                                                         break;
5304                                                 }
5305                                         }
5306                                         else if (typeof(cmp) == 'string' || typeof(cmp) == 'number')
5307                                         {
5308                                                 if (val != cmp)
5309                                                 {
5310                                                         rv = false;
5311                                                         break;
5312                                                 }
5313                                         }
5314                                         else if (typeof(cmp) == 'function')
5315                                         {
5316                                                 if (!cmp(val))
5317                                                 {
5318                                                         rv = false;
5319                                                         break;
5320                                                 }
5321                                         }
5322                                         else if (cmp instanceof RegExp)
5323                                         {
5324                                                 if (!cmp.test(val))
5325                                                 {
5326                                                         rv = false;
5327                                                         break;
5328                                                 }
5329                                         }
5330                                 }
5331
5332                                 if (rv)
5333                                 {
5334                                         if (i.disabled)
5335                                         {
5336                                                 i.disabled = false;
5337                                                 i.top.fadeIn();
5338                                         }
5339
5340                                         return true;
5341                                 }
5342                         }
5343
5344                         if (!i.disabled)
5345                         {
5346                                 i.disabled = true;
5347                                 i.top.is(':visible') ? i.top.fadeOut() : i.top.hide();
5348                         }
5349
5350                         return false;
5351                 }
5352         });
5353
5354         this.cbi.CheckboxValue = this.cbi.AbstractValue.extend({
5355                 widget: function(sid)
5356                 {
5357                         var o = this.options;
5358
5359                         if (typeof(o.enabled)  == 'undefined') o.enabled  = '1';
5360                         if (typeof(o.disabled) == 'undefined') o.disabled = '0';
5361
5362                         var i = $('<input />')
5363                                 .attr('id', this.id(sid))
5364                                 .attr('type', 'checkbox')
5365                                 .prop('checked', this.ucivalue(sid));
5366
5367                         return $('<div />')
5368                                 .addClass('checkbox')
5369                                 .append(this.validator(sid, i));
5370                 },
5371
5372                 ucivalue: function(sid)
5373                 {
5374                         var v = this.callSuper('ucivalue', sid);
5375
5376                         if (typeof(v) == 'boolean')
5377                                 return v;
5378
5379                         return (v == this.options.enabled);
5380                 },
5381
5382                 formvalue: function(sid)
5383                 {
5384                         var v = $('#' + this.id(sid)).prop('checked');
5385
5386                         if (typeof(v) == 'undefined')
5387                                 return !!this.options.initial;
5388
5389                         return v;
5390                 },
5391
5392                 save: function(sid)
5393                 {
5394                         var uci = this.ucipath(sid);
5395
5396                         if (this.instance[sid].disabled)
5397                         {
5398                                 if (!this.options.keep)
5399                                         return this.map.set(uci.config, uci.section, uci.option, undefined);
5400
5401                                 return false;
5402                         }
5403
5404                         var chg = this.changed(sid);
5405                         var val = this.formvalue(sid);
5406
5407                         if (chg)
5408                         {
5409                                 if (this.options.optional && val == this.options.initial)
5410                                         this.map.set(uci.config, uci.section, uci.option, undefined);
5411                                 else
5412                                         this.map.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
5413                         }
5414
5415                         return chg;
5416                 }
5417         });
5418
5419         this.cbi.InputValue = this.cbi.AbstractValue.extend({
5420                 widget: function(sid)
5421                 {
5422                         var i = $('<input />')
5423                                 .addClass('form-control')
5424                                 .attr('id', this.id(sid))
5425                                 .attr('type', 'text')
5426                                 .attr('placeholder', this.options.placeholder)
5427                                 .val(this.ucivalue(sid));
5428
5429                         return this.validator(sid, i);
5430                 }
5431         });
5432
5433         this.cbi.PasswordValue = this.cbi.AbstractValue.extend({
5434                 widget: function(sid)
5435                 {
5436                         var i = $('<input />')
5437                                 .addClass('form-control')
5438                                 .attr('id', this.id(sid))
5439                                 .attr('type', 'password')
5440                                 .attr('placeholder', this.options.placeholder)
5441                                 .val(this.ucivalue(sid));
5442
5443                         var t = $('<span />')
5444                                 .addClass('input-group-btn')
5445                                 .append(L.ui.button(L.tr('Reveal'), 'default')
5446                                         .click(function(ev) {
5447                                                 var b = $(this);
5448                                                 var i = b.parent().prev();
5449                                                 var t = i.attr('type');
5450                                                 b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal'));
5451                                                 i.attr('type', (t == 'password') ? 'text' : 'password');
5452                                                 b = i = t = null;
5453                                         }));
5454
5455                         this.validator(sid, i);
5456
5457                         return $('<div />')
5458                                 .addClass('input-group')
5459                                 .append(i)
5460                                 .append(t);
5461                 }
5462         });
5463
5464         this.cbi.ListValue = this.cbi.AbstractValue.extend({
5465                 widget: function(sid)
5466                 {
5467                         var s = $('<select />')
5468                                 .addClass('form-control');
5469
5470                         if (this.options.optional && !this.has_empty)
5471                                 $('<option />')
5472                                         .attr('value', '')
5473                                         .text(L.tr('-- Please choose --'))
5474                                         .appendTo(s);
5475
5476                         if (this.choices)
5477                                 for (var i = 0; i < this.choices.length; i++)
5478                                         $('<option />')
5479                                                 .attr('value', this.choices[i][0])
5480                                                 .text(this.choices[i][1])
5481                                                 .appendTo(s);
5482
5483                         s.attr('id', this.id(sid)).val(this.ucivalue(sid));
5484
5485                         return this.validator(sid, s);
5486                 },
5487
5488                 value: function(k, v)
5489                 {
5490                         if (!this.choices)
5491                                 this.choices = [ ];
5492
5493                         if (k == '')
5494                                 this.has_empty = true;
5495
5496                         this.choices.push([k, v || k]);
5497                         return this;
5498                 }
5499         });
5500
5501         this.cbi.MultiValue = this.cbi.ListValue.extend({
5502                 widget: function(sid)
5503                 {
5504                         var v = this.ucivalue(sid);
5505                         var t = $('<div />').attr('id', this.id(sid));
5506
5507                         if (!$.isArray(v))
5508                                 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5509
5510                         var s = { };
5511                         for (var i = 0; i < v.length; i++)
5512                                 s[v[i]] = true;
5513
5514                         if (this.choices)
5515                                 for (var i = 0; i < this.choices.length; i++)
5516                                 {
5517                                         $('<label />')
5518                                                 .addClass('checkbox')
5519                                                 .append($('<input />')
5520                                                         .attr('type', 'checkbox')
5521                                                         .attr('value', this.choices[i][0])
5522                                                         .prop('checked', s[this.choices[i][0]]))
5523                                                 .append(this.choices[i][1])
5524                                                 .appendTo(t);
5525                                 }
5526
5527                         return t;
5528                 },
5529
5530                 formvalue: function(sid)
5531                 {
5532                         var rv = [ ];
5533                         var fields = $('#' + this.id(sid) + ' > label > input');
5534
5535                         for (var i = 0; i < fields.length; i++)
5536                                 if (fields[i].checked)
5537                                         rv.push(fields[i].getAttribute('value'));
5538
5539                         return rv;
5540                 },
5541
5542                 textvalue: function(sid)
5543                 {
5544                         var v = this.formvalue(sid);
5545                         var c = { };
5546
5547                         if (this.choices)
5548                                 for (var i = 0; i < this.choices.length; i++)
5549                                         c[this.choices[i][0]] = this.choices[i][1];
5550
5551                         var t = [ ];
5552
5553                         for (var i = 0; i < v.length; i++)
5554                                 t.push(c[v[i]] || v[i]);
5555
5556                         return t.join(', ');
5557                 }
5558         });
5559
5560         this.cbi.ComboBox = this.cbi.AbstractValue.extend({
5561                 _change: function(ev)
5562                 {
5563                         var s = ev.target;
5564                         var self = ev.data.self;
5565
5566                         if (s.selectedIndex == (s.options.length - 1))
5567                         {
5568                                 ev.data.select.hide();
5569                                 ev.data.input.show().focus();
5570                                 ev.data.input.val('');
5571                         }
5572                         else if (self.options.optional && s.selectedIndex == 0)
5573                         {
5574                                 ev.data.input.val('');
5575                         }
5576                         else
5577                         {
5578                                 ev.data.input.val(ev.data.select.val());
5579                         }
5580
5581                         ev.stopPropagation();
5582                 },
5583
5584                 _blur: function(ev)
5585                 {
5586                         var seen = false;
5587                         var val = this.value;
5588                         var self = ev.data.self;
5589
5590                         ev.data.select.empty();
5591
5592                         if (self.options.optional && !self.has_empty)
5593                                 $('<option />')
5594                                         .attr('value', '')
5595                                         .text(L.tr('-- please choose --'))
5596                                         .appendTo(ev.data.select);
5597
5598                         if (self.choices)
5599                                 for (var i = 0; i < self.choices.length; i++)
5600                                 {
5601                                         if (self.choices[i][0] == val)
5602                                                 seen = true;
5603
5604                                         $('<option />')
5605                                                 .attr('value', self.choices[i][0])
5606                                                 .text(self.choices[i][1])
5607                                                 .appendTo(ev.data.select);
5608                                 }
5609
5610                         if (!seen && val != '')
5611                                 $('<option />')
5612                                         .attr('value', val)
5613                                         .text(val)
5614                                         .appendTo(ev.data.select);
5615
5616                         $('<option />')
5617                                 .attr('value', ' ')
5618                                 .text(L.tr('-- custom --'))
5619                                 .appendTo(ev.data.select);
5620
5621                         ev.data.input.hide();
5622                         ev.data.select.val(val).show().blur();
5623                 },
5624
5625                 _enter: function(ev)
5626                 {
5627                         if (ev.which != 13)
5628                                 return true;
5629
5630                         ev.preventDefault();
5631                         ev.data.self._blur(ev);
5632                         return false;
5633                 },
5634
5635                 widget: function(sid)
5636                 {
5637                         var d = $('<div />')
5638                                 .attr('id', this.id(sid));
5639
5640                         var t = $('<input />')
5641                                 .addClass('form-control')
5642                                 .attr('type', 'text')
5643                                 .hide()
5644                                 .appendTo(d);
5645
5646                         var s = $('<select />')
5647                                 .addClass('form-control')
5648                                 .appendTo(d);
5649
5650                         var evdata = {
5651                                 self: this,
5652                                 input: t,
5653                                 select: s
5654                         };
5655
5656                         s.change(evdata, this._change);
5657                         t.blur(evdata, this._blur);
5658                         t.keydown(evdata, this._enter);
5659
5660                         t.val(this.ucivalue(sid));
5661                         t.blur();
5662
5663                         this.validator(sid, t);
5664                         this.validator(sid, s);
5665
5666                         return d;
5667                 },
5668
5669                 value: function(k, v)
5670                 {
5671                         if (!this.choices)
5672                                 this.choices = [ ];
5673
5674                         if (k == '')
5675                                 this.has_empty = true;
5676
5677                         this.choices.push([k, v || k]);
5678                         return this;
5679                 },
5680
5681                 formvalue: function(sid)
5682                 {
5683                         var v = $('#' + this.id(sid)).children('input').val();
5684                         return (v == '') ? undefined : v;
5685                 }
5686         });
5687
5688         this.cbi.DynamicList = this.cbi.ComboBox.extend({
5689                 _redraw: function(focus, add, del, s)
5690                 {
5691                         var v = s.values || [ ];
5692                         delete s.values;
5693
5694                         $(s.parent).children('div.input-group').children('input').each(function(i) {
5695                                 if (i != del)
5696                                         v.push(this.value || '');
5697                         });
5698
5699                         $(s.parent).empty();
5700
5701                         if (add >= 0)
5702                         {
5703                                 focus = add + 1;
5704                                 v.splice(focus, 0, '');
5705                         }
5706                         else if (v.length == 0)
5707                         {
5708                                 focus = 0;
5709                                 v.push('');
5710                         }
5711
5712                         for (var i = 0; i < v.length; i++)
5713                         {
5714                                 var evdata = {
5715                                         sid: s.sid,
5716                                         self: s.self,
5717                                         parent: s.parent,
5718                                         index: i,
5719                                         remove: ((i+1) < v.length)
5720                                 };
5721
5722                                 var btn;
5723                                 if (evdata.remove)
5724                                         btn = L.ui.button('–', 'danger').click(evdata, this._btnclick);
5725                                 else
5726                                         btn = L.ui.button('+', 'success').click(evdata, this._btnclick);
5727
5728                                 if (this.choices)
5729                                 {
5730                                         var txt = $('<input />')
5731                                                 .addClass('form-control')
5732                                                 .attr('type', 'text')
5733                                                 .hide();
5734
5735                                         var sel = $('<select />')
5736                                                 .addClass('form-control');
5737
5738                                         $('<div />')
5739                                                 .addClass('input-group')
5740                                                 .append(txt)
5741                                                 .append(sel)
5742                                                 .append($('<span />')
5743                                                         .addClass('input-group-btn')
5744                                                         .append(btn))
5745                                                 .appendTo(s.parent);
5746
5747                                         evdata.input = this.validator(s.sid, txt, true);
5748                                         evdata.select = this.validator(s.sid, sel, true);
5749
5750                                         sel.change(evdata, this._change);
5751                                         txt.blur(evdata, this._blur);
5752                                         txt.keydown(evdata, this._keydown);
5753
5754                                         txt.val(v[i]);
5755                                         txt.blur();
5756
5757                                         if (i == focus || -(i+1) == focus)
5758                                                 sel.focus();
5759
5760                                         sel = txt = null;
5761                                 }
5762                                 else
5763                                 {
5764                                         var f = $('<input />')
5765                                                 .attr('type', 'text')
5766                                                 .attr('index', i)
5767                                                 .attr('placeholder', (i == 0) ? this.options.placeholder : '')
5768                                                 .addClass('form-control')
5769                                                 .keydown(evdata, this._keydown)
5770                                                 .keypress(evdata, this._keypress)
5771                                                 .val(v[i]);
5772
5773                                         $('<div />')
5774                                                 .addClass('input-group')
5775                                                 .append(f)
5776                                                 .append($('<span />')
5777                                                         .addClass('input-group-btn')
5778                                                         .append(btn))
5779                                                 .appendTo(s.parent);
5780
5781                                         if (i == focus)
5782                                         {
5783                                                 f.focus();
5784                                         }
5785                                         else if (-(i+1) == focus)
5786                                         {
5787                                                 f.focus();
5788
5789                                                 /* force cursor to end */
5790                                                 var val = f.val();
5791                                                 f.val(' ');
5792                                                 f.val(val);
5793                                         }
5794
5795                                         evdata.input = this.validator(s.sid, f, true);
5796
5797                                         f = null;
5798                                 }
5799
5800                                 evdata = null;
5801                         }
5802
5803                         s = null;
5804                 },
5805
5806                 _keypress: function(ev)
5807                 {
5808                         switch (ev.which)
5809                         {
5810                                 /* backspace, delete */
5811                                 case 8:
5812                                 case 46:
5813                                         if (ev.data.input.val() == '')
5814                                         {
5815                                                 ev.preventDefault();
5816                                                 return false;
5817                                         }
5818
5819                                         return true;
5820
5821                                 /* enter, arrow up, arrow down */
5822                                 case 13:
5823                                 case 38:
5824                                 case 40:
5825                                         ev.preventDefault();
5826                                         return false;
5827                         }
5828
5829                         return true;
5830                 },
5831
5832                 _keydown: function(ev)
5833                 {
5834                         var input = ev.data.input;
5835
5836                         switch (ev.which)
5837                         {
5838                                 /* backspace, delete */
5839                                 case 8:
5840                                 case 46:
5841                                         if (input.val().length == 0)
5842                                         {
5843                                                 ev.preventDefault();
5844
5845                                                 var index = ev.data.index;
5846                                                 var focus = index;
5847
5848                                                 if (ev.which == 8)
5849                                                         focus = -focus;
5850
5851                                                 ev.data.self._redraw(focus, -1, index, ev.data);
5852                                                 return false;
5853                                         }
5854
5855                                         break;
5856
5857                                 /* enter */
5858                                 case 13:
5859                                         ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
5860                                         break;
5861
5862                                 /* arrow up */
5863                                 case 38:
5864                                         var prev = input.parent().prevAll('div.input-group:first').children('input');
5865                                         if (prev.is(':visible'))
5866                                                 prev.focus();
5867                                         else
5868                                                 prev.next('select').focus();
5869                                         break;
5870
5871                                 /* arrow down */
5872                                 case 40:
5873                                         var next = input.parent().nextAll('div.input-group:first').children('input');
5874                                         if (next.is(':visible'))
5875                                                 next.focus();
5876                                         else
5877                                                 next.next('select').focus();
5878                                         break;
5879                         }
5880
5881                         return true;
5882                 },
5883
5884                 _btnclick: function(ev)
5885                 {
5886                         if (!this.getAttribute('disabled'))
5887                         {
5888                                 if (ev.data.remove)
5889                                 {
5890                                         var index = ev.data.index;
5891                                         ev.data.self._redraw(-index, -1, index, ev.data);
5892                                 }
5893                                 else
5894                                 {
5895                                         ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
5896                                 }
5897                         }
5898
5899                         return false;
5900                 },
5901
5902                 widget: function(sid)
5903                 {
5904                         this.options.optional = true;
5905
5906                         var v = this.ucivalue(sid);
5907
5908                         if (!$.isArray(v))
5909                                 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5910
5911                         var d = $('<div />')
5912                                 .attr('id', this.id(sid))
5913                                 .addClass('cbi-input-dynlist');
5914
5915                         this._redraw(NaN, -1, -1, {
5916                                 self:      this,
5917                                 parent:    d[0],
5918                                 values:    v,
5919                                 sid:       sid
5920                         });
5921
5922                         return d;
5923                 },
5924
5925                 ucivalue: function(sid)
5926                 {
5927                         var v = this.callSuper('ucivalue', sid);
5928
5929                         if (!$.isArray(v))
5930                                 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5931
5932                         return v;
5933                 },
5934
5935                 formvalue: function(sid)
5936                 {
5937                         var rv = [ ];
5938                         var fields = $('#' + this.id(sid) + ' input');
5939
5940                         for (var i = 0; i < fields.length; i++)
5941                                 if (typeof(fields[i].value) == 'string' && fields[i].value.length)
5942                                         rv.push(fields[i].value);
5943
5944                         return rv;
5945                 }
5946         });
5947
5948         this.cbi.DummyValue = this.cbi.AbstractValue.extend({
5949                 widget: function(sid)
5950                 {
5951                         return $('<div />')
5952                                 .addClass('form-control-static')
5953                                 .attr('id', this.id(sid))
5954                                 .html(this.ucivalue(sid));
5955                 },
5956
5957                 formvalue: function(sid)
5958                 {
5959                         return this.ucivalue(sid);
5960                 }
5961         });
5962
5963         this.cbi.ButtonValue = this.cbi.AbstractValue.extend({
5964                 widget: function(sid)
5965                 {
5966                         this.options.optional = true;
5967
5968                         var btn = $('<button />')
5969                                 .addClass('btn btn-default')
5970                                 .attr('id', this.id(sid))
5971                                 .attr('type', 'button')
5972                                 .text(this.label('text'));
5973
5974                         return this.validator(sid, btn);
5975                 }
5976         });
5977
5978         this.cbi.NetworkList = this.cbi.AbstractValue.extend({
5979                 load: function(sid)
5980                 {
5981                         return L.NetworkModel.init();
5982                 },
5983
5984                 _device_icon: function(dev)
5985                 {
5986                         return $('<img />')
5987                                 .attr('src', dev.icon())
5988                                 .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'));
5989                 },
5990
5991                 widget: function(sid)
5992                 {
5993                         var id = this.id(sid);
5994                         var ul = $('<ul />')
5995                                 .attr('id', id)
5996                                 .addClass('list-unstyled');
5997
5998                         var itype = this.options.multiple ? 'checkbox' : 'radio';
5999                         var value = this.ucivalue(sid);
6000                         var check = { };
6001
6002                         if (!this.options.multiple)
6003                                 check[value] = true;
6004                         else
6005                                 for (var i = 0; i < value.length; i++)
6006                                         check[value[i]] = true;
6007
6008                         var interfaces = L.NetworkModel.getInterfaces();
6009
6010                         for (var i = 0; i < interfaces.length; i++)
6011                         {
6012                                 var iface = interfaces[i];
6013                                 var badge = $('<span />')
6014                                         .addClass('badge')
6015                                         .text('%s: '.format(iface.name()));
6016
6017                                 var dev = iface.getDevice();
6018                                 var subdevs = iface.getSubdevices();
6019
6020                                 if (subdevs.length)
6021                                         for (var j = 0; j < subdevs.length; j++)
6022                                                 badge.append(this._device_icon(subdevs[j]));
6023                                 else if (dev)
6024                                         badge.append(this._device_icon(dev));
6025                                 else
6026                                         badge.append($('<em />').text(L.tr('(No devices attached)')));
6027
6028                                 $('<li />')
6029                                         .append($('<label />')
6030                                                 .addClass(itype + ' inline')
6031                                                 .append($('<input />')
6032                                                         .attr('name', itype + id)
6033                                                         .attr('type', itype)
6034                                                         .attr('value', iface.name())
6035                                                         .prop('checked', !!check[iface.name()]))
6036                                                 .append(badge))
6037                                         .appendTo(ul);
6038                         }
6039
6040                         if (!this.options.multiple)
6041                         {
6042                                 $('<li />')
6043                                         .append($('<label />')
6044                                                 .addClass(itype + ' inline text-muted')
6045                                                 .append($('<input />')
6046                                                         .attr('name', itype + id)
6047                                                         .attr('type', itype)
6048                                                         .attr('value', '')
6049                                                         .prop('checked', $.isEmptyObject(check)))
6050                                                 .append(L.tr('unspecified')))
6051                                         .appendTo(ul);
6052                         }
6053
6054                         return ul;
6055                 },
6056
6057                 ucivalue: function(sid)
6058                 {
6059                         var v = this.callSuper('ucivalue', sid);
6060
6061                         if (!this.options.multiple)
6062                         {
6063                                 if ($.isArray(v))
6064                                 {
6065                                         return v[0];
6066                                 }
6067                                 else if (typeof(v) == 'string')
6068                                 {
6069                                         v = v.match(/\S+/);
6070                                         return v ? v[0] : undefined;
6071                                 }
6072
6073                                 return v;
6074                         }
6075                         else
6076                         {
6077                                 if (typeof(v) == 'string')
6078                                         v = v.match(/\S+/g);
6079
6080                                 return v || [ ];
6081                         }
6082                 },
6083
6084                 formvalue: function(sid)
6085                 {
6086                         var inputs = $('#' + this.id(sid) + ' input');
6087
6088                         if (!this.options.multiple)
6089                         {
6090                                 for (var i = 0; i < inputs.length; i++)
6091                                         if (inputs[i].checked && inputs[i].value !== '')
6092                                                 return inputs[i].value;
6093
6094                                 return undefined;
6095                         }
6096
6097                         var rv = [ ];
6098
6099                         for (var i = 0; i < inputs.length; i++)
6100                                 if (inputs[i].checked)
6101                                         rv.push(inputs[i].value);
6102
6103                         return rv.length ? rv : undefined;
6104                 }
6105         });
6106
6107         this.cbi.DeviceList = this.cbi.NetworkList.extend({
6108                 _ev_focus: function(ev)
6109                 {
6110                         var self = ev.data.self;
6111                         var input = $(this);
6112
6113                         input.parent().prev().prop('checked', true);
6114                 },
6115
6116                 _ev_blur: function(ev)
6117                 {
6118                         ev.which = 10;
6119                         ev.data.self._ev_keydown.call(this, ev);
6120                 },
6121
6122                 _ev_keydown: function(ev)
6123                 {
6124                         if (ev.which != 10 && ev.which != 13)
6125                                 return;
6126
6127                         var sid = ev.data.sid;
6128                         var self = ev.data.self;
6129                         var input = $(this);
6130                         var ifnames = L.toArray(input.val());
6131
6132                         if (!ifnames.length)
6133                                 return;
6134
6135                         L.NetworkModel.createDevice(ifnames[0]);
6136
6137                         self._redraw(sid, $('#' + self.id(sid)), ifnames[0]);
6138                 },
6139
6140                 load: function(sid)
6141                 {
6142                         return L.NetworkModel.init();
6143                 },
6144
6145                 _redraw: function(sid, ul, sel)
6146                 {
6147                         var id = ul.attr('id');
6148                         var devs = L.NetworkModel.getDevices();
6149                         var iface = L.NetworkModel.getInterface(sid);
6150                         var itype = this.options.multiple ? 'checkbox' : 'radio';
6151                         var check = { };
6152
6153                         if (!sel)
6154                         {
6155                                 for (var i = 0; i < devs.length; i++)
6156                                         if (devs[i].isInNetwork(iface))
6157                                                 check[devs[i].name()] = true;
6158                         }
6159                         else
6160                         {
6161                                 if (this.options.multiple)
6162                                         check = L.toObject(this.formvalue(sid));
6163
6164                                 check[sel] = true;
6165                         }
6166
6167                         ul.empty();
6168
6169                         for (var i = 0; i < devs.length; i++)
6170                         {
6171                                 var dev = devs[i];
6172
6173                                 if (dev.isBridge() && this.options.bridges === false)
6174                                         continue;
6175
6176                                 if (!dev.isBridgeable() && this.options.multiple)
6177                                         continue;
6178
6179                                 var badge = $('<span />')
6180                                         .addClass('badge')
6181                                         .append($('<img />').attr('src', dev.icon()))
6182                                         .append(' %s: %s'.format(dev.name(), dev.description()));
6183
6184                                 //var ifcs = dev.getInterfaces();
6185                                 //if (ifcs.length)
6186                                 //{
6187                                 //      for (var j = 0; j < ifcs.length; j++)
6188                                 //              badge.append((j ? ', ' : ' (') + ifcs[j].name());
6189                                 //
6190                                 //      badge.append(')');
6191                                 //}
6192
6193                                 $('<li />')
6194                                         .append($('<label />')
6195                                                 .addClass(itype + ' inline')
6196                                                 .append($('<input />')
6197                                                         .attr('name', itype + id)
6198                                                         .attr('type', itype)
6199                                                         .attr('value', dev.name())
6200                                                         .prop('checked', !!check[dev.name()]))
6201                                                 .append(badge))
6202                                         .appendTo(ul);
6203                         }
6204
6205
6206                         $('<li />')
6207                                 .append($('<label />')
6208                                         .attr('for', 'custom' + id)
6209                                         .addClass(itype + ' inline')
6210                                         .append($('<input />')
6211                                                 .attr('name', itype + id)
6212                                                 .attr('type', itype)
6213                                                 .attr('value', ''))
6214                                         .append($('<span />')
6215                                                 .addClass('badge')
6216                                                 .append($('<input />')
6217                                                         .attr('id', 'custom' + id)
6218                                                         .attr('type', 'text')
6219                                                         .attr('placeholder', L.tr('Custom device â€¦'))
6220                                                         .on('focus', { self: this, sid: sid }, this._ev_focus)
6221                                                         .on('blur', { self: this, sid: sid }, this._ev_blur)
6222                                                         .on('keydown', { self: this, sid: sid }, this._ev_keydown))))
6223                                 .appendTo(ul);
6224
6225                         if (!this.options.multiple)
6226                         {
6227                                 $('<li />')
6228                                         .append($('<label />')
6229                                                 .addClass(itype + ' inline text-muted')
6230                                                 .append($('<input />')
6231                                                         .attr('name', itype + id)
6232                                                         .attr('type', itype)
6233                                                         .attr('value', '')
6234                                                         .prop('checked', $.isEmptyObject(check)))
6235                                                 .append(L.tr('unspecified')))
6236                                         .appendTo(ul);
6237                         }
6238                 },
6239
6240                 widget: function(sid)
6241                 {
6242                         var id = this.id(sid);
6243                         var ul = $('<ul />')
6244                                 .attr('id', id)
6245                                 .addClass('list-unstyled');
6246
6247                         this._redraw(sid, ul);
6248
6249                         return ul;
6250                 },
6251
6252                 save: function(sid)
6253                 {
6254                         if (this.instance[sid].disabled)
6255                                 return;
6256
6257                         var ifnames = this.formvalue(sid);
6258                         //if (!ifnames)
6259                         //      return;
6260
6261                         var iface = L.NetworkModel.getInterface(sid);
6262                         if (!iface)
6263                                 return;
6264
6265                         iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]);
6266                 }
6267         });
6268
6269
6270         this.cbi.AbstractSection = this.ui.AbstractWidget.extend({
6271                 id: function()
6272                 {
6273                         var s = [ arguments[0], this.map.uci_package, this.uci_type ];
6274
6275                         for (var i = 1; i < arguments.length; i++)
6276                                 s.push(arguments[i].replace(/\./g, '_'));
6277
6278                         return s.join('_');
6279                 },
6280
6281                 option: function(widget, name, options)
6282                 {
6283                         if (this.tabs.length == 0)
6284                                 this.tab({ id: '__default__', selected: true });
6285
6286                         return this.taboption('__default__', widget, name, options);
6287                 },
6288
6289                 tab: function(options)
6290                 {
6291                         if (options.selected)
6292                                 this.tabs.selected = this.tabs.length;
6293
6294                         this.tabs.push({
6295                                 id:          options.id,
6296                                 caption:     options.caption,
6297                                 description: options.description,
6298                                 fields:      [ ],
6299                                 li:          { }
6300                         });
6301                 },
6302
6303                 taboption: function(tabid, widget, name, options)
6304                 {
6305                         var tab;
6306                         for (var i = 0; i < this.tabs.length; i++)
6307                         {
6308                                 if (this.tabs[i].id == tabid)
6309                                 {
6310                                         tab = this.tabs[i];
6311                                         break;
6312                                 }
6313                         }
6314
6315                         if (!tab)
6316                                 throw 'Cannot append to unknown tab ' + tabid;
6317
6318                         var w = widget ? new widget(name, options) : null;
6319
6320                         if (!(w instanceof L.cbi.AbstractValue))
6321                                 throw 'Widget must be an instance of AbstractValue';
6322
6323                         w.section = this;
6324                         w.map     = this.map;
6325
6326                         this.fields[name] = w;
6327                         tab.fields.push(w);
6328
6329                         return w;
6330                 },
6331
6332                 tabtoggle: function(sid)
6333                 {
6334                         for (var i = 0; i < this.tabs.length; i++)
6335                         {
6336                                 var tab = this.tabs[i];
6337                                 var elem = $('#' + this.id('nodetab', sid, tab.id));
6338                                 var empty = true;
6339
6340                                 for (var j = 0; j < tab.fields.length; j++)
6341                                 {
6342                                         if (tab.fields[j].active(sid))
6343                                         {
6344                                                 empty = false;
6345                                                 break;
6346                                         }
6347                                 }
6348
6349                                 if (empty && elem.is(':visible'))
6350                                         elem.fadeOut();
6351                                 else if (!empty)
6352                                         elem.fadeIn();
6353                         }
6354                 },
6355
6356                 ucipackages: function(pkg)
6357                 {
6358                         for (var i = 0; i < this.tabs.length; i++)
6359                                 for (var j = 0; j < this.tabs[i].fields.length; j++)
6360                                         if (this.tabs[i].fields[j].options.uci_package)
6361                                                 pkg[this.tabs[i].fields[j].options.uci_package] = true;
6362                 },
6363
6364                 formvalue: function()
6365                 {
6366                         var rv = { };
6367
6368                         this.sections(function(s) {
6369                                 var sid = s['.name'];
6370                                 var sv = rv[sid] || (rv[sid] = { });
6371
6372                                 for (var i = 0; i < this.tabs.length; i++)
6373                                         for (var j = 0; j < this.tabs[i].fields.length; j++)
6374                                         {
6375                                                 var val = this.tabs[i].fields[j].formvalue(sid);
6376                                                 sv[this.tabs[i].fields[j].name] = val;
6377                                         }
6378                         });
6379
6380                         return rv;
6381                 },
6382
6383                 validate_section: function(sid)
6384                 {
6385                         var inst = this.instance[sid];
6386
6387                         var invals = 0;
6388                         var badge = $('#' + this.id('teaser', sid)).children('span:first');
6389
6390                         for (var i = 0; i < this.tabs.length; i++)
6391                         {
6392                                 var inval = 0;
6393                                 var stbadge = $('#' + this.id('nodetab', sid, this.tabs[i].id)).children('span:first');
6394
6395                                 for (var j = 0; j < this.tabs[i].fields.length; j++)
6396                                         if (!this.tabs[i].fields[j].validate(sid))
6397                                                 inval++;
6398
6399                                 if (inval > 0)
6400                                         stbadge.show()
6401                                                 .text(inval)
6402                                                 .attr('title', L.trp('1 Error', '%d Errors', inval).format(inval));
6403                                 else
6404                                         stbadge.hide();
6405
6406                                 invals += inval;
6407                         }
6408
6409                         if (invals > 0)
6410                                 badge.show()
6411                                         .text(invals)
6412                                         .attr('title', L.trp('1 Error', '%d Errors', invals).format(invals));
6413                         else
6414                                 badge.hide();
6415
6416                         return invals;
6417                 },
6418
6419                 validate: function()
6420                 {
6421                         var errors = 0;
6422                         var as = this.sections();
6423
6424                         for (var i = 0; i < as.length; i++)
6425                         {
6426                                 var invals = this.validate_section(as[i]['.name']);
6427
6428                                 if (invals > 0)
6429                                         errors += invals;
6430                         }
6431
6432                         var badge = $('#' + this.id('sectiontab')).children('span:first');
6433
6434                         if (errors > 0)
6435                                 badge.show()
6436                                         .text(errors)
6437                                         .attr('title', L.trp('1 Error', '%d Errors', errors).format(errors));
6438                         else
6439                                 badge.hide();
6440
6441                         return (errors == 0);
6442                 }
6443         });
6444
6445         this.cbi.TypedSection = this.cbi.AbstractSection.extend({
6446                 init: function(uci_type, options)
6447                 {
6448                         this.uci_type = uci_type;
6449                         this.options  = options;
6450                         this.tabs     = [ ];
6451                         this.fields   = { };
6452                         this.active_panel = 0;
6453                         this.active_tab   = { };
6454                 },
6455
6456                 filter: function(section)
6457                 {
6458                         return true;
6459                 },
6460
6461                 sections: function(cb)
6462                 {
6463                         var s1 = L.uci.sections(this.map.uci_package);
6464                         var s2 = [ ];
6465
6466                         for (var i = 0; i < s1.length; i++)
6467                                 if (s1[i]['.type'] == this.uci_type)
6468                                         if (this.filter(s1[i]))
6469                                                 s2.push(s1[i]);
6470
6471                         if (typeof(cb) == 'function')
6472                                 for (var i = 0; i < s2.length; i++)
6473                                         cb.call(this, s2[i]);
6474
6475                         return s2;
6476                 },
6477
6478                 add: function(name)
6479                 {
6480                         return this.map.add(this.map.uci_package, this.uci_type, name);
6481                 },
6482
6483                 remove: function(sid)
6484                 {
6485                         return this.map.remove(this.map.uci_package, sid);
6486                 },
6487
6488                 _ev_add: function(ev)
6489                 {
6490                         var addb = $(this);
6491                         var name = undefined;
6492                         var self = ev.data.self;
6493
6494                         if (addb.prev().prop('nodeName') == 'INPUT')
6495                                 name = addb.prev().val();
6496
6497                         if (addb.prop('disabled') || name === '')
6498                                 return;
6499
6500                         L.ui.saveScrollTop();
6501
6502                         self.active_panel = -1;
6503                         self.map.save();
6504
6505                         ev.data.sid  = self.add(name);
6506                         ev.data.type = self.uci_type;
6507                         ev.data.name = name;
6508
6509                         self.trigger('add', ev);
6510
6511                         self.map.redraw();
6512
6513                         L.ui.restoreScrollTop();
6514                 },
6515
6516                 _ev_remove: function(ev)
6517                 {
6518                         var self = ev.data.self;
6519                         var sid  = ev.data.sid;
6520
6521                         L.ui.saveScrollTop();
6522
6523                         self.trigger('remove', ev);
6524
6525                         self.map.save();
6526                         self.remove(sid);
6527                         self.map.redraw();
6528
6529                         L.ui.restoreScrollTop();
6530
6531                         ev.stopPropagation();
6532                 },
6533
6534                 _ev_sid: function(ev)
6535                 {
6536                         var self = ev.data.self;
6537                         var text = $(this);
6538                         var addb = text.next();
6539                         var errt = addb.next();
6540                         var name = text.val();
6541
6542                         if (!/^[a-zA-Z0-9_]*$/.test(name))
6543                         {
6544                                 errt.text(L.tr('Invalid section name')).show();
6545                                 text.addClass('error');
6546                                 addb.prop('disabled', true);
6547                                 return false;
6548                         }
6549
6550                         if (L.uci.get(self.map.uci_package, name))
6551                         {
6552                                 errt.text(L.tr('Name already used')).show();
6553                                 text.addClass('error');
6554                                 addb.prop('disabled', true);
6555                                 return false;
6556                         }
6557
6558                         errt.text('').hide();
6559                         text.removeClass('error');
6560                         addb.prop('disabled', false);
6561                         return true;
6562                 },
6563
6564                 _ev_tab: function(ev)
6565                 {
6566                         var self = ev.data.self;
6567                         var sid  = ev.data.sid;
6568
6569                         self.validate();
6570                         self.active_tab[sid] = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
6571                 },
6572
6573                 _ev_panel_collapse: function(ev)
6574                 {
6575                         var self = ev.data.self;
6576
6577                         var this_panel = $(ev.target);
6578                         var this_toggle = this_panel.prevAll('[data-toggle="collapse"]:first');
6579
6580                         var prev_toggle = $($(ev.delegateTarget).find('[data-toggle="collapse"]:eq(%d)'.format(self.active_panel)));
6581                         var prev_panel = $(prev_toggle.attr('data-target'));
6582
6583                         prev_panel
6584                                 .removeClass('in')
6585                                 .addClass('collapse');
6586
6587                         prev_toggle.find('.luci2-section-teaser')
6588                                 .show()
6589                                 .children('span:last')
6590                                 .empty()
6591                                 .append(self.teaser(prev_panel.attr('data-luci2-sid')));
6592
6593                         this_toggle.find('.luci2-section-teaser')
6594                                 .hide();
6595
6596                         self.active_panel = parseInt(this_panel.attr('data-luci2-panel-index'));
6597                         self.validate();
6598                 },
6599
6600                 _ev_panel_open: function(ev)
6601                 {
6602                         var self  = ev.data.self;
6603                         var panel = $($(this).attr('data-target'));
6604                         var index = parseInt(panel.attr('data-luci2-panel-index'));
6605
6606                         if (index == self.active_panel)
6607                                 ev.stopPropagation();
6608                 },
6609
6610                 _ev_sort: function(ev)
6611                 {
6612                         var self    = ev.data.self;
6613                         var cur_idx = ev.data.index;
6614                         var new_idx = cur_idx + (ev.data.up ? -1 : 1);
6615                         var s       = self.sections();
6616
6617                         if (new_idx >= 0 && new_idx < s.length)
6618                         {
6619                                 L.uci.swap(self.map.uci_package, s[cur_idx]['.name'], s[new_idx]['.name']);
6620
6621                                 self.map.save();
6622                                 self.map.redraw();
6623                         }
6624
6625                         ev.stopPropagation();
6626                 },
6627
6628                 teaser: function(sid)
6629                 {
6630                         var tf = this.teaser_fields;
6631
6632                         if (!tf)
6633                         {
6634                                 tf = this.teaser_fields = [ ];
6635
6636                                 if ($.isArray(this.options.teasers))
6637                                 {
6638                                         for (var i = 0; i < this.options.teasers.length; i++)
6639                                         {
6640                                                 var f = this.options.teasers[i];
6641                                                 if (f instanceof L.cbi.AbstractValue)
6642                                                         tf.push(f);
6643                                                 else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue)
6644                                                         tf.push(this.fields[f]);
6645                                         }
6646                                 }
6647                                 else
6648                                 {
6649                                         for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
6650                                                 for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
6651                                                         tf.push(this.tabs[i].fields[j]);
6652                                 }
6653                         }
6654
6655                         var t = '';
6656
6657                         for (var i = 0; i < tf.length; i++)
6658                         {
6659                                 if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
6660                                         continue;
6661
6662                                 var n = tf[i].options.caption || tf[i].name;
6663                                 var v = tf[i].textvalue(sid);
6664
6665                                 if (typeof(v) == 'undefined')
6666                                         continue;
6667
6668                                 t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
6669                         }
6670
6671                         return t;
6672                 },
6673
6674                 _render_add: function()
6675                 {
6676                         if (!this.options.addremove)
6677                                 return null;
6678
6679                         var text = L.tr('Add section');
6680                         var ttip = L.tr('Create new section...');
6681
6682                         if ($.isArray(this.options.add_caption))
6683                                 text = this.options.add_caption[0], ttip = this.options.add_caption[1];
6684                         else if (typeof(this.options.add_caption) == 'string')
6685                                 text = this.options.add_caption, ttip = '';
6686
6687                         var add = $('<div />');
6688
6689                         if (this.options.anonymous === false)
6690                         {
6691                                 $('<input />')
6692                                         .addClass('cbi-input-text')
6693                                         .attr('type', 'text')
6694                                         .attr('placeholder', ttip)
6695                                         .blur({ self: this }, this._ev_sid)
6696                                         .keyup({ self: this }, this._ev_sid)
6697                                         .appendTo(add);
6698
6699                                 $('<img />')
6700                                         .attr('src', L.globals.resource + '/icons/cbi/add.gif')
6701                                         .attr('title', text)
6702                                         .addClass('cbi-button')
6703                                         .click({ self: this }, this._ev_add)
6704                                         .appendTo(add);
6705
6706                                 $('<div />')
6707                                         .addClass('cbi-value-error')
6708                                         .hide()
6709                                         .appendTo(add);
6710                         }
6711                         else
6712                         {
6713                                 L.ui.button(text, 'success', ttip)
6714                                         .click({ self: this }, this._ev_add)
6715                                         .appendTo(add);
6716                         }
6717
6718                         return add;
6719                 },
6720
6721                 _render_remove: function(sid, index)
6722                 {
6723                         if (!this.options.addremove)
6724                                 return null;
6725
6726                         var text = L.tr('Remove');
6727                         var ttip = L.tr('Remove this section');
6728
6729                         if ($.isArray(this.options.remove_caption))
6730                                 text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
6731                         else if (typeof(this.options.remove_caption) == 'string')
6732                                 text = this.options.remove_caption, ttip = '';
6733
6734                         return L.ui.button(text, 'danger', ttip)
6735                                 .click({ self: this, sid: sid, index: index }, this._ev_remove);
6736                 },
6737
6738                 _render_sort: function(sid, index)
6739                 {
6740                         if (!this.options.sortable)
6741                                 return null;
6742
6743                         var b1 = L.ui.button('↑', 'info', L.tr('Move up'))
6744                                 .click({ self: this, index: index, up: true }, this._ev_sort);
6745
6746                         var b2 = L.ui.button('↓', 'info', L.tr('Move down'))
6747                                 .click({ self: this, index: index, up: false }, this._ev_sort);
6748
6749                         return b1.add(b2);
6750                 },
6751
6752                 _render_caption: function()
6753                 {
6754                         return $('<h3 />')
6755                                 .addClass('panel-title')
6756                                 .append(this.label('caption') || this.uci_type);
6757                 },
6758
6759                 _render_description: function()
6760                 {
6761                         var text = this.label('description');
6762
6763                         if (text)
6764                                 return $('<div />')
6765                                         .addClass('luci2-section-description')
6766                                         .text(text);
6767
6768                         return null;
6769                 },
6770
6771                 _render_teaser: function(sid, index)
6772                 {
6773                         if (this.options.collabsible || this.map.options.collabsible)
6774                         {
6775                                 return $('<div />')
6776                                         .attr('id', this.id('teaser', sid))
6777                                         .addClass('luci2-section-teaser well well-sm')
6778                                         .append($('<span />')
6779                                                 .addClass('badge'))
6780                                         .append($('<span />'));
6781                         }
6782
6783                         return null;
6784                 },
6785
6786                 _render_head: function(condensed)
6787                 {
6788                         if (condensed)
6789                                 return null;
6790
6791                         return $('<div />')
6792                                 .addClass('panel-heading')
6793                                 .append(this._render_caption())
6794                                 .append(this._render_description());
6795                 },
6796
6797                 _render_tab_description: function(sid, index, tab_index)
6798                 {
6799                         var tab = this.tabs[tab_index];
6800
6801                         if (typeof(tab.description) == 'string')
6802                         {
6803                                 return $('<div />')
6804                                         .addClass('cbi-tab-descr')
6805                                         .text(tab.description);
6806                         }
6807
6808                         return null;
6809                 },
6810
6811                 _render_tab_head: function(sid, index, tab_index)
6812                 {
6813                         var tab = this.tabs[tab_index];
6814                         var cur = this.active_tab[sid] || 0;
6815
6816                         var tabh = $('<li />')
6817                                 .append($('<a />')
6818                                         .attr('id', this.id('nodetab', sid, tab.id))
6819                                         .attr('href', '#' + this.id('node', sid, tab.id))
6820                                         .attr('data-toggle', 'tab')
6821                                         .attr('data-luci2-tab-index', tab_index)
6822                                         .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ')
6823                                         .append($('<span />')
6824                                                 .addClass('badge'))
6825                                         .on('shown.bs.tab', { self: this, sid: sid }, this._ev_tab));
6826
6827                         if (cur == tab_index)
6828                                 tabh.addClass('active');
6829
6830                         if (!tab.fields.length)
6831                                 tabh.hide();
6832
6833                         return tabh;
6834                 },
6835
6836                 _render_tab_body: function(sid, index, tab_index)
6837                 {
6838                         var tab = this.tabs[tab_index];
6839                         var cur = this.active_tab[sid] || 0;
6840
6841                         var tabb = $('<div />')
6842                                 .addClass('tab-pane')
6843                                 .attr('id', this.id('node', sid, tab.id))
6844                                 .attr('data-luci2-tab-index', tab_index)
6845                                 .append(this._render_tab_description(sid, index, tab_index));
6846
6847                         if (cur == tab_index)
6848                                 tabb.addClass('active');
6849
6850                         for (var i = 0; i < tab.fields.length; i++)
6851                                 tabb.append(tab.fields[i].render(sid));
6852
6853                         return tabb;
6854                 },
6855
6856                 _render_section_head: function(sid, index)
6857                 {
6858                         var head = $('<div />')
6859                                 .addClass('luci2-section-header')
6860                                 .append(this._render_teaser(sid, index))
6861                                 .append($('<div />')
6862                                         .addClass('btn-group')
6863                                         .append(this._render_sort(sid, index))
6864                                         .append(this._render_remove(sid, index)));
6865
6866                         if (this.options.collabsible)
6867                         {
6868                                 head.attr('data-toggle', 'collapse')
6869                                         .attr('data-parent', this.id('sectiongroup'))
6870                                         .attr('data-target', '#' + this.id('panel', sid))
6871                                         .on('click', { self: this }, this._ev_panel_open);
6872                         }
6873
6874                         return head;
6875                 },
6876
6877                 _render_section_body: function(sid, index)
6878                 {
6879                         var body = $('<div />')
6880                                 .attr('id', this.id('panel', sid))
6881                                 .attr('data-luci2-panel-index', index)
6882                                 .attr('data-luci2-sid', sid);
6883
6884                         if (this.options.collabsible || this.map.options.collabsible)
6885                         {
6886                                 body.addClass('panel-collapse collapse');
6887
6888                                 if (index == this.active_panel)
6889                                         body.addClass('in');
6890                         }
6891
6892                         var tab_heads = $('<ul />')
6893                                 .addClass('nav nav-tabs');
6894
6895                         var tab_bodies = $('<div />')
6896                                 .addClass('form-horizontal tab-content')
6897                                 .append(tab_heads);
6898
6899                         for (var j = 0; j < this.tabs.length; j++)
6900                         {
6901                                 tab_heads.append(this._render_tab_head(sid, index, j));
6902                                 tab_bodies.append(this._render_tab_body(sid, index, j));
6903                         }
6904
6905                         body.append(tab_bodies);
6906
6907                         if (this.tabs.length <= 1)
6908                                 tab_heads.hide();
6909
6910                         return body;
6911                 },
6912
6913                 _render_body: function(condensed)
6914                 {
6915                         var s = this.sections();
6916
6917                         if (this.active_panel < 0)
6918                                 this.active_panel += s.length;
6919                         else if (this.active_panel >= s.length)
6920                                 this.active_panel = s.length - 1;
6921
6922                         var body = $('<ul />')
6923                                 .addClass('list-group');
6924
6925                         if (this.options.collabsible)
6926                         {
6927                                 body.attr('id', this.id('sectiongroup'))
6928                                         .on('show.bs.collapse', { self: this }, this._ev_panel_collapse);
6929                         }
6930
6931                         if (s.length == 0)
6932                         {
6933                                 body.append($('<li />')
6934                                         .addClass('list-group-item text-muted')
6935                                         .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))
6936                         }
6937
6938                         for (var i = 0; i < s.length; i++)
6939                         {
6940                                 var sid = s[i]['.name'];
6941                                 var inst = this.instance[sid] = { tabs: [ ] };
6942
6943                                 body.append($('<li />')
6944                                         .addClass('list-group-item')
6945                                         .append(this._render_section_head(sid, i))
6946                                         .append(this._render_section_body(sid, i)));
6947                         }
6948
6949                         return body;
6950                 },
6951
6952                 render: function(condensed)
6953                 {
6954                         this.instance = { };
6955
6956                         var panel = $('<div />')
6957                                 .addClass('panel panel-default')
6958                                 .append(this._render_head(condensed))
6959                                 .append(this._render_body(condensed));
6960
6961                         if (this.options.addremove)
6962                                 panel.append($('<div />')
6963                                         .addClass('panel-footer')
6964                                         .append(this._render_add()));
6965
6966                         return panel;
6967                 },
6968
6969                 finish: function()
6970                 {
6971                         var s = this.sections();
6972
6973                         for (var i = 0; i < s.length; i++)
6974                         {
6975                                 var sid = s[i]['.name'];
6976
6977                                 this.validate_section(sid);
6978
6979                                 if (i != this.active_panel)
6980                                         $('#' + this.id('teaser', sid)).children('span:last')
6981                                                 .append(this.teaser(sid));
6982                                 else
6983                                         $('#' + this.id('teaser', sid))
6984                                                 .hide();
6985                         }
6986                 }
6987         });
6988
6989         this.cbi.TableSection = this.cbi.TypedSection.extend({
6990                 _render_table_head: function()
6991                 {
6992                         var thead = $('<thead />')
6993                                 .append($('<tr />')
6994                                         .addClass('cbi-section-table-titles'));
6995
6996                         for (var j = 0; j < this.tabs[0].fields.length; j++)
6997                                 thead.children().append($('<th />')
6998                                         .addClass('cbi-section-table-cell')
6999                                         .css('width', this.tabs[0].fields[j].options.width || '')
7000                                         .append(this.tabs[0].fields[j].label('caption')));
7001
7002                         if (this.options.addremove !== false || this.options.sortable)
7003                                 thead.children().append($('<th />')
7004                                         .addClass('cbi-section-table-cell')
7005                                         .text(' '));
7006
7007                         return thead;
7008                 },
7009
7010                 _render_table_row: function(sid, index)
7011                 {
7012                         var row = $('<tr />')
7013                                 .attr('data-luci2-sid', sid);
7014
7015                         for (var j = 0; j < this.tabs[0].fields.length; j++)
7016                         {
7017                                 row.append($('<td />')
7018                                         .css('width', this.tabs[0].fields[j].options.width || '')
7019                                         .append(this.tabs[0].fields[j].render(sid, true)));
7020                         }
7021
7022                         if (this.options.addremove !== false || this.options.sortable)
7023                         {
7024                                 row.append($('<td />')
7025                                         .addClass('text-right')
7026                                         .append($('<div />')
7027                                                 .addClass('btn-group')
7028                                                 .append(this._render_sort(sid, index))
7029                                                 .append(this._render_remove(sid, index))));
7030                         }
7031
7032                         return row;
7033                 },
7034
7035                 _render_table_body: function()
7036                 {
7037                         var s = this.sections();
7038
7039                         var tbody = $('<tbody />');
7040
7041                         if (s.length == 0)
7042                         {
7043                                 var cols = this.tabs[0].fields.length;
7044
7045                                 if (this.options.addremove !== false || this.options.sortable)
7046                                         cols++;
7047
7048                                 tbody.append($('<tr />')
7049                                         .append($('<td />')
7050                                                 .addClass('text-muted')
7051                                                 .attr('colspan', cols)
7052                                                 .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
7053                         }
7054
7055                         for (var i = 0; i < s.length; i++)
7056                         {
7057                                 var sid = s[i]['.name'];
7058                                 var inst = this.instance[sid] = { tabs: [ ] };
7059
7060                                 tbody.append(this._render_table_row(sid, i));
7061                         }
7062
7063                         return tbody;
7064                 },
7065
7066                 _render_body: function(condensed)
7067                 {
7068                         return $('<table />')
7069                                 .addClass('table table-condensed table-hover')
7070                                 .append(this._render_table_head())
7071                                 .append(this._render_table_body());
7072                 }
7073         });
7074
7075         this.cbi.NamedSection = this.cbi.TypedSection.extend({
7076                 sections: function(cb)
7077                 {
7078                         var sa = [ ];
7079                         var sl = L.uci.sections(this.map.uci_package);
7080
7081                         for (var i = 0; i < sl.length; i++)
7082                                 if (sl[i]['.name'] == this.uci_type)
7083                                 {
7084                                         sa.push(sl[i]);
7085                                         break;
7086                                 }
7087
7088                         if (typeof(cb) == 'function' && sa.length > 0)
7089                                 cb.call(this, sa[0]);
7090
7091                         return sa;
7092                 }
7093         });
7094
7095         this.cbi.SingleSection = this.cbi.NamedSection.extend({
7096                 render: function()
7097                 {
7098                         this.instance = { };
7099                         this.instance[this.uci_type] = { tabs: [ ] };
7100
7101                         return this._render_section_body(this.uci_type, 0);
7102                 }
7103         });
7104
7105         this.cbi.DummySection = this.cbi.TypedSection.extend({
7106                 sections: function(cb)
7107                 {
7108                         if (typeof(cb) == 'function')
7109                                 cb.apply(this, [ { '.name': this.uci_type } ]);
7110
7111                         return [ { '.name': this.uci_type } ];
7112                 }
7113         });
7114
7115         this.cbi.Map = this.ui.AbstractWidget.extend({
7116                 init: function(uci_package, options)
7117                 {
7118                         var self = this;
7119
7120                         this.uci_package = uci_package;
7121                         this.sections = [ ];
7122                         this.options = L.defaults(options, {
7123                                 save:    function() { },
7124                                 prepare: function() { }
7125                         });
7126                 },
7127
7128                 _load_cb: function()
7129                 {
7130                         var deferreds = [ L.deferrable(this.options.prepare()) ];
7131
7132                         for (var i = 0; i < this.sections.length; i++)
7133                         {
7134                                 for (var f in this.sections[i].fields)
7135                                 {
7136                                         if (typeof(this.sections[i].fields[f].load) != 'function')
7137                                                 continue;
7138
7139                                         var s = this.sections[i].sections();
7140                                         for (var j = 0; j < s.length; j++)
7141                                         {
7142                                                 var rv = this.sections[i].fields[f].load(s[j]['.name']);
7143                                                 if (L.isDeferred(rv))
7144                                                         deferreds.push(rv);
7145                                         }
7146                                 }
7147                         }
7148
7149                         return $.when.apply($, deferreds);
7150                 },
7151
7152                 load: function()
7153                 {
7154                         var self = this;
7155                         var packages = { };
7156
7157                         for (var i = 0; i < this.sections.length; i++)
7158                                 this.sections[i].ucipackages(packages);
7159
7160                         packages[this.uci_package] = true;
7161
7162                         for (var pkg in packages)
7163                                 if (!L.uci.writable(pkg))
7164                                         this.options.readonly = true;
7165
7166                         return L.uci.load(L.toArray(packages)).then(function() {
7167                                 return self._load_cb();
7168                         });
7169                 },
7170
7171                 _ev_tab: function(ev)
7172                 {
7173                         var self = ev.data.self;
7174
7175                         self.validate();
7176                         self.active_tab = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
7177                 },
7178
7179                 _ev_apply: function(ev)
7180                 {
7181                         var self = ev.data.self;
7182
7183                         self.trigger('apply', ev);
7184                 },
7185
7186                 _ev_save: function(ev)
7187                 {
7188                         var self = ev.data.self;
7189
7190                         self.send().then(function() {
7191                                 self.trigger('save', ev);
7192                         });
7193                 },
7194
7195                 _ev_reset: function(ev)
7196                 {
7197                         var self = ev.data.self;
7198
7199                         self.trigger('reset', ev);
7200                         self.reset();
7201                 },
7202
7203                 _render_tab_head: function(tab_index)
7204                 {
7205                         var section = this.sections[tab_index];
7206                         var cur = this.active_tab || 0;
7207
7208                         var tabh = $('<li />')
7209                                 .append($('<a />')
7210                                         .attr('id', section.id('sectiontab'))
7211                                         .attr('href', '#' + section.id('section'))
7212                                         .attr('data-toggle', 'tab')
7213                                         .attr('data-luci2-tab-index', tab_index)
7214                                         .text(section.label('caption') + ' ')
7215                                         .append($('<span />')
7216                                                 .addClass('badge'))
7217                                         .on('shown.bs.tab', { self: this }, this._ev_tab));
7218
7219                         if (cur == tab_index)
7220                                 tabh.addClass('active');
7221
7222                         return tabh;
7223                 },
7224
7225                 _render_tab_body: function(tab_index)
7226                 {
7227                         var section = this.sections[tab_index];
7228                         var desc = section.label('description');
7229                         var cur = this.active_tab || 0;
7230
7231                         var tabb = $('<div />')
7232                                 .addClass('tab-pane')
7233                                 .attr('id', section.id('section'))
7234                                 .attr('data-luci2-tab-index', tab_index);
7235
7236                         if (cur == tab_index)
7237                                 tabb.addClass('active');
7238
7239                         if (desc)
7240                                 tabb.append($('<p />')
7241                                         .text(desc));
7242
7243                         var s = section.render(this.options.tabbed);
7244
7245                         if (this.options.readonly || section.options.readonly)
7246                                 s.find('input, select, button, img.cbi-button').attr('disabled', true);
7247
7248                         tabb.append(s);
7249
7250                         return tabb;
7251                 },
7252
7253                 _render_body: function()
7254                 {
7255                         var tabs = $('<ul />')
7256                                 .addClass('nav nav-tabs');
7257
7258                         var body = $('<div />')
7259                                 .append(tabs);
7260
7261                         for (var i = 0; i < this.sections.length; i++)
7262                         {
7263                                 tabs.append(this._render_tab_head(i));
7264                                 body.append(this._render_tab_body(i));
7265                         }
7266
7267                         if (this.options.tabbed)
7268                                 body.addClass('tab-content');
7269                         else
7270                                 tabs.hide();
7271
7272                         return body;
7273                 },
7274
7275                 _render_footer: function()
7276                 {
7277                         var evdata = {
7278                                 self: this
7279                         };
7280
7281                         return $('<div />')
7282                                 .addClass('panel panel-default panel-body text-right')
7283                                 .append($('<div />')
7284                                         .addClass('btn-group')
7285                                         .append(L.ui.button(L.tr('Save & Apply'), 'primary')
7286                                                 .click(evdata, this._ev_apply))
7287                                         .append(L.ui.button(L.tr('Save'), 'default')
7288                                                 .click(evdata, this._ev_save))
7289                                         .append(L.ui.button(L.tr('Reset'), 'default')
7290                                                 .click(evdata, this._ev_reset)));
7291                 },
7292
7293                 render: function()
7294                 {
7295                         var map = $('<form />');
7296
7297                         if (typeof(this.options.caption) == 'string')
7298                                 map.append($('<h2 />')
7299                                         .text(this.options.caption));
7300
7301                         if (typeof(this.options.description) == 'string')
7302                                 map.append($('<p />')
7303                                         .text(this.options.description));
7304
7305                         map.append(this._render_body());
7306
7307                         if (this.options.pageaction !== false)
7308                                 map.append(this._render_footer());
7309
7310                         return map;
7311                 },
7312
7313                 finish: function()
7314                 {
7315                         for (var i = 0; i < this.sections.length; i++)
7316                                 this.sections[i].finish();
7317
7318                         this.validate();
7319                 },
7320
7321                 redraw: function()
7322                 {
7323                         this.target.hide().empty().append(this.render());
7324                         this.finish();
7325                         this.target.show();
7326                 },
7327
7328                 section: function(widget, uci_type, options)
7329                 {
7330                         var w = widget ? new widget(uci_type, options) : null;
7331
7332                         if (!(w instanceof L.cbi.AbstractSection))
7333                                 throw 'Widget must be an instance of AbstractSection';
7334
7335                         w.map = this;
7336                         w.index = this.sections.length;
7337
7338                         this.sections.push(w);
7339                         return w;
7340                 },
7341
7342                 formvalue: function()
7343                 {
7344                         var rv = { };
7345
7346                         for (var i = 0; i < this.sections.length; i++)
7347                         {
7348                                 var sids = this.sections[i].formvalue();
7349                                 for (var sid in sids)
7350                                 {
7351                                         var s = rv[sid] || (rv[sid] = { });
7352                                         $.extend(s, sids[sid]);
7353                                 }
7354                         }
7355
7356                         return rv;
7357                 },
7358
7359                 add: function(conf, type, name)
7360                 {
7361                         return L.uci.add(conf, type, name);
7362                 },
7363
7364                 remove: function(conf, sid)
7365                 {
7366                         return L.uci.remove(conf, sid);
7367                 },
7368
7369                 get: function(conf, sid, opt)
7370                 {
7371                         return L.uci.get(conf, sid, opt);
7372                 },
7373
7374                 set: function(conf, sid, opt, val)
7375                 {
7376                         return L.uci.set(conf, sid, opt, val);
7377                 },
7378
7379                 validate: function()
7380                 {
7381                         var rv = true;
7382
7383                         for (var i = 0; i < this.sections.length; i++)
7384                         {
7385                                 if (!this.sections[i].validate())
7386                                         rv = false;
7387                         }
7388
7389                         return rv;
7390                 },
7391
7392                 save: function()
7393                 {
7394                         var self = this;
7395
7396                         if (self.options.readonly)
7397                                 return L.deferrable();
7398
7399                         var deferreds = [ ];
7400
7401                         for (var i = 0; i < self.sections.length; i++)
7402                         {
7403                                 if (self.sections[i].options.readonly)
7404                                         continue;
7405
7406                                 for (var f in self.sections[i].fields)
7407                                 {
7408                                         if (typeof(self.sections[i].fields[f].save) != 'function')
7409                                                 continue;
7410
7411                                         var s = self.sections[i].sections();
7412                                         for (var j = 0; j < s.length; j++)
7413                                         {
7414                                                 var rv = self.sections[i].fields[f].save(s[j]['.name']);
7415                                                 if (L.isDeferred(rv))
7416                                                         deferreds.push(rv);
7417                                         }
7418                                 }
7419                         }
7420
7421                         return $.when.apply($, deferreds).then(function() {
7422                                 return L.deferrable(self.options.save());
7423                         });
7424                 },
7425
7426                 send: function()
7427                 {
7428                         if (!this.validate())
7429                                 return L.deferrable();
7430
7431                         var self = this;
7432
7433                         L.ui.saveScrollTop();
7434                         L.ui.loading(true);
7435
7436                         return this.save().then(function() {
7437                                 return L.uci.save();
7438                         }).then(function() {
7439                                 return L.ui.updateChanges();
7440                         }).then(function() {
7441                                 return self.load();
7442                         }).then(function() {
7443                                 self.redraw();
7444                                 self = null;
7445
7446                                 L.ui.loading(false);
7447                                 L.ui.restoreScrollTop();
7448                         });
7449                 },
7450
7451                 revert: function()
7452                 {
7453                         var packages = { };
7454
7455                         for (var i = 0; i < this.sections.length; i++)
7456                                 this.sections[i].ucipackages(packages);
7457
7458                         packages[this.uci_package] = true;
7459
7460                         L.uci.unload(L.toArray(packages));
7461                 },
7462
7463                 reset: function()
7464                 {
7465                         var self = this;
7466
7467                         self.revert();
7468
7469                         return self.insertInto(self.target);
7470                 },
7471
7472                 insertInto: function(id)
7473                 {
7474                         var self = this;
7475                             self.target = $(id);
7476
7477                         L.ui.loading(true);
7478                         self.target.hide();
7479
7480                         return self.load().then(function() {
7481                                 self.target.empty().append(self.render());
7482                                 self.finish();
7483                                 self.target.show();
7484                                 self = null;
7485                                 L.ui.loading(false);
7486                         });
7487                 }
7488         });
7489
7490         this.cbi.Modal = this.cbi.Map.extend({
7491                 _ev_apply: function(ev)
7492                 {
7493                         var self = ev.data.self;
7494
7495                         self.trigger('apply', ev);
7496                 },
7497
7498                 _ev_save: function(ev)
7499                 {
7500                         var self = ev.data.self;
7501
7502                         self.send().then(function() {
7503                                 self.trigger('save', ev);
7504                                 self.close();
7505                         });
7506                 },
7507
7508                 _ev_reset: function(ev)
7509                 {
7510                         var self = ev.data.self;
7511
7512                         self.trigger('close', ev);
7513                         self.revert();
7514                         self.close();
7515                 },
7516
7517                 _render_footer: function()
7518                 {
7519                         var evdata = {
7520                                 self: this
7521                         };
7522
7523                         return $('<div />')
7524                                 .addClass('btn-group')
7525                                 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
7526                                         .click(evdata, this._ev_apply))
7527                                 .append(L.ui.button(L.tr('Save'), 'default')
7528                                         .click(evdata, this._ev_save))
7529                                 .append(L.ui.button(L.tr('Cancel'), 'default')
7530                                         .click(evdata, this._ev_reset));
7531                 },
7532
7533                 render: function()
7534                 {
7535                         var modal = L.ui.dialog(this.label('caption'), null, { wide: true });
7536                         var map = $('<form />');
7537
7538                         var desc = this.label('description');
7539                         if (desc)
7540                                 map.append($('<p />').text(desc));
7541
7542                         map.append(this._render_body());
7543
7544                         modal.find('.modal-body').append(map);
7545                         modal.find('.modal-footer').append(this._render_footer());
7546
7547                         return modal;
7548                 },
7549
7550                 redraw: function()
7551                 {
7552                         this.render();
7553                         this.finish();
7554                 },
7555
7556                 show: function()
7557                 {
7558                         var self = this;
7559
7560                         L.ui.loading(true);
7561
7562                         return self.load().then(function() {
7563                                 self.render();
7564                                 self.finish();
7565
7566                                 L.ui.loading(false);
7567                         });
7568                 },
7569
7570                 close: function()
7571                 {
7572                         L.ui.dialog(false);
7573                 }
7574         });
7575 };