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