luci2: rework datatype validators to use new global parseIPv4(), parseIPv6() and...
[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
5017                                 .append(this.widget(sid))
5018                                 .append(i.error)
5019                                 .appendTo(i.top);
5020
5021                         if (!condensed)
5022                         {
5023                                 i.widget.addClass('col-lg-5');
5024
5025                                 $('<div />')
5026                                         .addClass('col-lg-5')
5027                                         .text((typeof(this.options.description) == 'string') ? this.options.description : '')
5028                                         .appendTo(i.top);
5029                         }
5030
5031                         return i.top;
5032                 },
5033
5034                 active: function(sid)
5035                 {
5036                         return (this.instance[sid] && !this.instance[sid].disabled);
5037                 },
5038
5039                 ucipath: function(sid)
5040                 {
5041                         return {
5042                                 config:  (this.options.uci_package || this.map.uci_package),
5043                                 section: (this.options.uci_section || sid),
5044                                 option:  (this.options.uci_option  || this.name)
5045                         };
5046                 },
5047
5048                 ucivalue: function(sid)
5049                 {
5050                         var uci = this.ucipath(sid);
5051                         var val = this.map.get(uci.config, uci.section, uci.option);
5052
5053                         if (typeof(val) == 'undefined')
5054                                 return this.options.initial;
5055
5056                         return val;
5057                 },
5058
5059                 formvalue: function(sid)
5060                 {
5061                         var v = $('#' + this.id(sid)).val();
5062                         return (v === '') ? undefined : v;
5063                 },
5064
5065                 textvalue: function(sid)
5066                 {
5067                         var v = this.formvalue(sid);
5068
5069                         if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
5070                                 v = this.ucivalue(sid);
5071
5072                         if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
5073                                 v = this.options.placeholder;
5074
5075                         if (typeof(v) == 'undefined' || v === '')
5076                                 return undefined;
5077
5078                         if (typeof(v) == 'string' && $.isArray(this.choices))
5079                         {
5080                                 for (var i = 0; i < this.choices.length; i++)
5081                                         if (v === this.choices[i][0])
5082                                                 return this.choices[i][1];
5083                         }
5084                         else if (v === true)
5085                                 return L.tr('yes');
5086                         else if (v === false)
5087                                 return L.tr('no');
5088                         else if ($.isArray(v))
5089                                 return v.join(', ');
5090
5091                         return v;
5092                 },
5093
5094                 changed: function(sid)
5095                 {
5096                         var a = this.ucivalue(sid);
5097                         var b = this.formvalue(sid);
5098
5099                         if (typeof(a) != typeof(b))
5100                                 return true;
5101
5102                         if (typeof(a) == 'object')
5103                         {
5104                                 if (a.length != b.length)
5105                                         return true;
5106
5107                                 for (var i = 0; i < a.length; i++)
5108                                         if (a[i] != b[i])
5109                                                 return true;
5110
5111                                 return false;
5112                         }
5113
5114                         return (a != b);
5115                 },
5116
5117                 save: function(sid)
5118                 {
5119                         var uci = this.ucipath(sid);
5120
5121                         if (this.instance[sid].disabled)
5122                         {
5123                                 if (!this.options.keep)
5124                                         return this.map.set(uci.config, uci.section, uci.option, undefined);
5125
5126                                 return false;
5127                         }
5128
5129                         var chg = this.changed(sid);
5130                         var val = this.formvalue(sid);
5131
5132                         if (chg)
5133                                 this.map.set(uci.config, uci.section, uci.option, val);
5134
5135                         return chg;
5136                 },
5137
5138                 _ev_validate: function(ev)
5139                 {
5140                         var d = ev.data;
5141                         var rv = true;
5142                         var val = d.elem.val();
5143                         var vstack = d.vstack;
5144
5145                         if (vstack && typeof(vstack[0]) == 'function')
5146                         {
5147                                 delete validation.message;
5148
5149                                 if ((val.length == 0 && !d.opt))
5150                                 {
5151                                         d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
5152                                         d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
5153
5154                                         d.inst.error.text(L.tr('Field must not be empty')).show();
5155                                         rv = false;
5156                                 }
5157                                 else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
5158                                 {
5159                                         d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
5160                                         d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
5161
5162                                         d.inst.error.text(validation.message.format.apply(validation.message, vstack[1])).show();
5163                                         rv = false;
5164                                 }
5165                                 else
5166                                 {
5167                                         d.elem.parents('div.form-group, td').first().removeClass('luci2-form-error');
5168                                         d.elem.parents('div.input-group, div.form-group, td').first().removeClass('has-error');
5169
5170                                         if (d.multi && d.inst.widget && d.inst.widget.find('input.error, select.error').length > 0)
5171                                                 rv = false;
5172                                         else
5173                                                 d.inst.error.text('').hide();
5174                                 }
5175                         }
5176
5177                         if (rv)
5178                         {
5179                                 for (var field in d.self.rdependency)
5180                                         d.self.rdependency[field].toggle(d.sid);
5181
5182                                 d.self.section.tabtoggle(d.sid);
5183                         }
5184
5185                         return rv;
5186                 },
5187
5188                 validator: function(sid, elem, multi)
5189                 {
5190                         var evdata = {
5191                                 self:   this,
5192                                 sid:    sid,
5193                                 elem:   elem,
5194                                 multi:  multi,
5195                                 inst:   this.instance[sid],
5196                                 opt:    this.options.optional
5197                         };
5198
5199                         if (this.events)
5200                                 for (var evname in this.events)
5201                                         elem.on(evname, evdata, this.events[evname]);
5202
5203                         if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
5204                                 return elem;
5205
5206                         var vstack;
5207                         if (typeof(this.options.datatype) == 'string')
5208                         {
5209                                 try {
5210                                         evdata.vstack = L.cbi.validation.compile(this.options.datatype);
5211                                 } catch(e) { };
5212                         }
5213                         else if (typeof(this.options.datatype) == 'function')
5214                         {
5215                                 var vfunc = this.options.datatype;
5216                                 evdata.vstack = [ function(elem) {
5217                                         var rv = vfunc(this, elem);
5218                                         if (rv !== true)
5219                                                 validation.message = rv;
5220                                         return (rv === true);
5221                                 }, [ elem ] ];
5222                         }
5223
5224                         if (elem.prop('tagName') == 'SELECT')
5225                         {
5226                                 elem.change(evdata, this._ev_validate);
5227                         }
5228                         else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
5229                         {
5230                                 elem.click(evdata, this._ev_validate);
5231                                 elem.blur(evdata, this._ev_validate);
5232                         }
5233                         else
5234                         {
5235                                 elem.keyup(evdata, this._ev_validate);
5236                                 elem.blur(evdata, this._ev_validate);
5237                         }
5238
5239                         elem.attr('cbi-validate', true).on('validate', evdata, this._ev_validate);
5240
5241                         return elem;
5242                 },
5243
5244                 validate: function(sid)
5245                 {
5246                         var i = this.instance[sid];
5247
5248                         i.widget.find('[cbi-validate]').trigger('validate');
5249
5250                         return (i.disabled || i.error.text() == '');
5251                 },
5252
5253                 depends: function(d, v, add)
5254                 {
5255                         var dep;
5256
5257                         if ($.isArray(d))
5258                         {
5259                                 dep = { };
5260                                 for (var i = 0; i < d.length; i++)
5261                                 {
5262                                         if (typeof(d[i]) == 'string')
5263                                                 dep[d[i]] = true;
5264                                         else if (d[i] instanceof L.cbi.AbstractValue)
5265                                                 dep[d[i].name] = true;
5266                                 }
5267                         }
5268                         else if (d instanceof L.cbi.AbstractValue)
5269                         {
5270                                 dep = { };
5271                                 dep[d.name] = (typeof(v) == 'undefined') ? true : v;
5272                         }
5273                         else if (typeof(d) == 'object')
5274                         {
5275                                 dep = d;
5276                         }
5277                         else if (typeof(d) == 'string')
5278                         {
5279                                 dep = { };
5280                                 dep[d] = (typeof(v) == 'undefined') ? true : v;
5281                         }
5282
5283                         if (!dep || $.isEmptyObject(dep))
5284                                 return this;
5285
5286                         for (var field in dep)
5287                         {
5288                                 var f = this.section.fields[field];
5289                                 if (f)
5290                                         f.rdependency[this.name] = this;
5291                                 else
5292                                         delete dep[field];
5293                         }
5294
5295                         if ($.isEmptyObject(dep))
5296                                 return this;
5297
5298                         if (!add || !this.dependencies.length)
5299                                 this.dependencies.push(dep);
5300                         else
5301                                 for (var i = 0; i < this.dependencies.length; i++)
5302                                         $.extend(this.dependencies[i], dep);
5303
5304                         return this;
5305                 },
5306
5307                 toggle: function(sid)
5308                 {
5309                         var d = this.dependencies;
5310                         var i = this.instance[sid];
5311
5312                         if (!d.length)
5313                                 return true;
5314
5315                         for (var n = 0; n < d.length; n++)
5316                         {
5317                                 var rv = true;
5318
5319                                 for (var field in d[n])
5320                                 {
5321                                         var val = this.section.fields[field].formvalue(sid);
5322                                         var cmp = d[n][field];
5323
5324                                         if (typeof(cmp) == 'boolean')
5325                                         {
5326                                                 if (cmp == (typeof(val) == 'undefined' || val === '' || val === false))
5327                                                 {
5328                                                         rv = false;
5329                                                         break;
5330                                                 }
5331                                         }
5332                                         else if (typeof(cmp) == 'string' || typeof(cmp) == 'number')
5333                                         {
5334                                                 if (val != cmp)
5335                                                 {
5336                                                         rv = false;
5337                                                         break;
5338                                                 }
5339                                         }
5340                                         else if (typeof(cmp) == 'function')
5341                                         {
5342                                                 if (!cmp(val))
5343                                                 {
5344                                                         rv = false;
5345                                                         break;
5346                                                 }
5347                                         }
5348                                         else if (cmp instanceof RegExp)
5349                                         {
5350                                                 if (!cmp.test(val))
5351                                                 {
5352                                                         rv = false;
5353                                                         break;
5354                                                 }
5355                                         }
5356                                 }
5357
5358                                 if (rv)
5359                                 {
5360                                         if (i.disabled)
5361                                         {
5362                                                 i.disabled = false;
5363                                                 i.top.fadeIn();
5364                                         }
5365
5366                                         return true;
5367                                 }
5368                         }
5369
5370                         if (!i.disabled)
5371                         {
5372                                 i.disabled = true;
5373                                 i.top.is(':visible') ? i.top.fadeOut() : i.top.hide();
5374                         }
5375
5376                         return false;
5377                 }
5378         });
5379
5380         this.cbi.CheckboxValue = this.cbi.AbstractValue.extend({
5381                 widget: function(sid)
5382                 {
5383                         var o = this.options;
5384
5385                         if (typeof(o.enabled)  == 'undefined') o.enabled  = '1';
5386                         if (typeof(o.disabled) == 'undefined') o.disabled = '0';
5387
5388                         var i = $('<input />')
5389                                 .attr('id', this.id(sid))
5390                                 .attr('type', 'checkbox')
5391                                 .prop('checked', this.ucivalue(sid));
5392
5393                         return $('<div />')
5394                                 .addClass('checkbox')
5395                                 .append(this.validator(sid, i));
5396                 },
5397
5398                 ucivalue: function(sid)
5399                 {
5400                         var v = this.callSuper('ucivalue', sid);
5401
5402                         if (typeof(v) == 'boolean')
5403                                 return v;
5404
5405                         return (v == this.options.enabled);
5406                 },
5407
5408                 formvalue: function(sid)
5409                 {
5410                         var v = $('#' + this.id(sid)).prop('checked');
5411
5412                         if (typeof(v) == 'undefined')
5413                                 return !!this.options.initial;
5414
5415                         return v;
5416                 },
5417
5418                 save: function(sid)
5419                 {
5420                         var uci = this.ucipath(sid);
5421
5422                         if (this.instance[sid].disabled)
5423                         {
5424                                 if (!this.options.keep)
5425                                         return this.map.set(uci.config, uci.section, uci.option, undefined);
5426
5427                                 return false;
5428                         }
5429
5430                         var chg = this.changed(sid);
5431                         var val = this.formvalue(sid);
5432
5433                         if (chg)
5434                         {
5435                                 if (this.options.optional && val == this.options.initial)
5436                                         this.map.set(uci.config, uci.section, uci.option, undefined);
5437                                 else
5438                                         this.map.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
5439                         }
5440
5441                         return chg;
5442                 }
5443         });
5444
5445         this.cbi.InputValue = this.cbi.AbstractValue.extend({
5446                 widget: function(sid)
5447                 {
5448                         var i = $('<input />')
5449                                 .addClass('form-control')
5450                                 .attr('id', this.id(sid))
5451                                 .attr('type', 'text')
5452                                 .attr('placeholder', this.options.placeholder)
5453                                 .val(this.ucivalue(sid));
5454
5455                         return this.validator(sid, i);
5456                 }
5457         });
5458
5459         this.cbi.PasswordValue = this.cbi.AbstractValue.extend({
5460                 widget: function(sid)
5461                 {
5462                         var i = $('<input />')
5463                                 .addClass('form-control')
5464                                 .attr('id', this.id(sid))
5465                                 .attr('type', 'password')
5466                                 .attr('placeholder', this.options.placeholder)
5467                                 .val(this.ucivalue(sid));
5468
5469                         var t = $('<span />')
5470                                 .addClass('input-group-btn')
5471                                 .append(L.ui.button(L.tr('Reveal'), 'default')
5472                                         .click(function(ev) {
5473                                                 var b = $(this);
5474                                                 var i = b.parent().prev();
5475                                                 var t = i.attr('type');
5476                                                 b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal'));
5477                                                 i.attr('type', (t == 'password') ? 'text' : 'password');
5478                                                 b = i = t = null;
5479                                         }));
5480
5481                         this.validator(sid, i);
5482
5483                         return $('<div />')
5484                                 .addClass('input-group')
5485                                 .append(i)
5486                                 .append(t);
5487                 }
5488         });
5489
5490         this.cbi.ListValue = this.cbi.AbstractValue.extend({
5491                 widget: function(sid)
5492                 {
5493                         var s = $('<select />')
5494                                 .addClass('form-control');
5495
5496                         if (this.options.optional && !this.has_empty)
5497                                 $('<option />')
5498                                         .attr('value', '')
5499                                         .text(L.tr('-- Please choose --'))
5500                                         .appendTo(s);
5501
5502                         if (this.choices)
5503                                 for (var i = 0; i < this.choices.length; i++)
5504                                         $('<option />')
5505                                                 .attr('value', this.choices[i][0])
5506                                                 .text(this.choices[i][1])
5507                                                 .appendTo(s);
5508
5509                         s.attr('id', this.id(sid)).val(this.ucivalue(sid));
5510
5511                         return this.validator(sid, s);
5512                 },
5513
5514                 value: function(k, v)
5515                 {
5516                         if (!this.choices)
5517                                 this.choices = [ ];
5518
5519                         if (k == '')
5520                                 this.has_empty = true;
5521
5522                         this.choices.push([k, v || k]);
5523                         return this;
5524                 }
5525         });
5526
5527         this.cbi.MultiValue = this.cbi.ListValue.extend({
5528                 widget: function(sid)
5529                 {
5530                         var v = this.ucivalue(sid);
5531                         var t = $('<div />').attr('id', this.id(sid));
5532
5533                         if (!$.isArray(v))
5534                                 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5535
5536                         var s = { };
5537                         for (var i = 0; i < v.length; i++)
5538                                 s[v[i]] = true;
5539
5540                         if (this.choices)
5541                                 for (var i = 0; i < this.choices.length; i++)
5542                                 {
5543                                         $('<label />')
5544                                                 .addClass('checkbox')
5545                                                 .append($('<input />')
5546                                                         .attr('type', 'checkbox')
5547                                                         .attr('value', this.choices[i][0])
5548                                                         .prop('checked', s[this.choices[i][0]]))
5549                                                 .append(this.choices[i][1])
5550                                                 .appendTo(t);
5551                                 }
5552
5553                         return t;
5554                 },
5555
5556                 formvalue: function(sid)
5557                 {
5558                         var rv = [ ];
5559                         var fields = $('#' + this.id(sid) + ' > label > input');
5560
5561                         for (var i = 0; i < fields.length; i++)
5562                                 if (fields[i].checked)
5563                                         rv.push(fields[i].getAttribute('value'));
5564
5565                         return rv;
5566                 },
5567
5568                 textvalue: function(sid)
5569                 {
5570                         var v = this.formvalue(sid);
5571                         var c = { };
5572
5573                         if (this.choices)
5574                                 for (var i = 0; i < this.choices.length; i++)
5575                                         c[this.choices[i][0]] = this.choices[i][1];
5576
5577                         var t = [ ];
5578
5579                         for (var i = 0; i < v.length; i++)
5580                                 t.push(c[v[i]] || v[i]);
5581
5582                         return t.join(', ');
5583                 }
5584         });
5585
5586         this.cbi.ComboBox = this.cbi.AbstractValue.extend({
5587                 _change: function(ev)
5588                 {
5589                         var s = ev.target;
5590                         var self = ev.data.self;
5591
5592                         if (s.selectedIndex == (s.options.length - 1))
5593                         {
5594                                 ev.data.select.hide();
5595                                 ev.data.input.show().focus();
5596                                 ev.data.input.val('');
5597                         }
5598                         else if (self.options.optional && s.selectedIndex == 0)
5599                         {
5600                                 ev.data.input.val('');
5601                         }
5602                         else
5603                         {
5604                                 ev.data.input.val(ev.data.select.val());
5605                         }
5606
5607                         ev.stopPropagation();
5608                 },
5609
5610                 _blur: function(ev)
5611                 {
5612                         var seen = false;
5613                         var val = this.value;
5614                         var self = ev.data.self;
5615
5616                         ev.data.select.empty();
5617
5618                         if (self.options.optional && !self.has_empty)
5619                                 $('<option />')
5620                                         .attr('value', '')
5621                                         .text(L.tr('-- please choose --'))
5622                                         .appendTo(ev.data.select);
5623
5624                         if (self.choices)
5625                                 for (var i = 0; i < self.choices.length; i++)
5626                                 {
5627                                         if (self.choices[i][0] == val)
5628                                                 seen = true;
5629
5630                                         $('<option />')
5631                                                 .attr('value', self.choices[i][0])
5632                                                 .text(self.choices[i][1])
5633                                                 .appendTo(ev.data.select);
5634                                 }
5635
5636                         if (!seen && val != '')
5637                                 $('<option />')
5638                                         .attr('value', val)
5639                                         .text(val)
5640                                         .appendTo(ev.data.select);
5641
5642                         $('<option />')
5643                                 .attr('value', ' ')
5644                                 .text(L.tr('-- custom --'))
5645                                 .appendTo(ev.data.select);
5646
5647                         ev.data.input.hide();
5648                         ev.data.select.val(val).show().blur();
5649                 },
5650
5651                 _enter: function(ev)
5652                 {
5653                         if (ev.which != 13)
5654                                 return true;
5655
5656                         ev.preventDefault();
5657                         ev.data.self._blur(ev);
5658                         return false;
5659                 },
5660
5661                 widget: function(sid)
5662                 {
5663                         var d = $('<div />')
5664                                 .attr('id', this.id(sid));
5665
5666                         var t = $('<input />')
5667                                 .addClass('form-control')
5668                                 .attr('type', 'text')
5669                                 .hide()
5670                                 .appendTo(d);
5671
5672                         var s = $('<select />')
5673                                 .addClass('form-control')
5674                                 .appendTo(d);
5675
5676                         var evdata = {
5677                                 self: this,
5678                                 input: t,
5679                                 select: s
5680                         };
5681
5682                         s.change(evdata, this._change);
5683                         t.blur(evdata, this._blur);
5684                         t.keydown(evdata, this._enter);
5685
5686                         t.val(this.ucivalue(sid));
5687                         t.blur();
5688
5689                         this.validator(sid, t);
5690                         this.validator(sid, s);
5691
5692                         return d;
5693                 },
5694
5695                 value: function(k, v)
5696                 {
5697                         if (!this.choices)
5698                                 this.choices = [ ];
5699
5700                         if (k == '')
5701                                 this.has_empty = true;
5702
5703                         this.choices.push([k, v || k]);
5704                         return this;
5705                 },
5706
5707                 formvalue: function(sid)
5708                 {
5709                         var v = $('#' + this.id(sid)).children('input').val();
5710                         return (v == '') ? undefined : v;
5711                 }
5712         });
5713
5714         this.cbi.DynamicList = this.cbi.ComboBox.extend({
5715                 _redraw: function(focus, add, del, s)
5716                 {
5717                         var v = s.values || [ ];
5718                         delete s.values;
5719
5720                         $(s.parent).children('div.input-group').children('input').each(function(i) {
5721                                 if (i != del)
5722                                         v.push(this.value || '');
5723                         });
5724
5725                         $(s.parent).empty();
5726
5727                         if (add >= 0)
5728                         {
5729                                 focus = add + 1;
5730                                 v.splice(focus, 0, '');
5731                         }
5732                         else if (v.length == 0)
5733                         {
5734                                 focus = 0;
5735                                 v.push('');
5736                         }
5737
5738                         for (var i = 0; i < v.length; i++)
5739                         {
5740                                 var evdata = {
5741                                         sid: s.sid,
5742                                         self: s.self,
5743                                         parent: s.parent,
5744                                         index: i,
5745                                         remove: ((i+1) < v.length)
5746                                 };
5747
5748                                 var btn;
5749                                 if (evdata.remove)
5750                                         btn = L.ui.button('–', 'danger').click(evdata, this._btnclick);
5751                                 else
5752                                         btn = L.ui.button('+', 'success').click(evdata, this._btnclick);
5753
5754                                 if (this.choices)
5755                                 {
5756                                         var txt = $('<input />')
5757                                                 .addClass('form-control')
5758                                                 .attr('type', 'text')
5759                                                 .hide();
5760
5761                                         var sel = $('<select />')
5762                                                 .addClass('form-control');
5763
5764                                         $('<div />')
5765                                                 .addClass('input-group')
5766                                                 .append(txt)
5767                                                 .append(sel)
5768                                                 .append($('<span />')
5769                                                         .addClass('input-group-btn')
5770                                                         .append(btn))
5771                                                 .appendTo(s.parent);
5772
5773                                         evdata.input = this.validator(s.sid, txt, true);
5774                                         evdata.select = this.validator(s.sid, sel, true);
5775
5776                                         sel.change(evdata, this._change);
5777                                         txt.blur(evdata, this._blur);
5778                                         txt.keydown(evdata, this._keydown);
5779
5780                                         txt.val(v[i]);
5781                                         txt.blur();
5782
5783                                         if (i == focus || -(i+1) == focus)
5784                                                 sel.focus();
5785
5786                                         sel = txt = null;
5787                                 }
5788                                 else
5789                                 {
5790                                         var f = $('<input />')
5791                                                 .attr('type', 'text')
5792                                                 .attr('index', i)
5793                                                 .attr('placeholder', (i == 0) ? this.options.placeholder : '')
5794                                                 .addClass('form-control')
5795                                                 .keydown(evdata, this._keydown)
5796                                                 .keypress(evdata, this._keypress)
5797                                                 .val(v[i]);
5798
5799                                         $('<div />')
5800                                                 .addClass('input-group')
5801                                                 .append(f)
5802                                                 .append($('<span />')
5803                                                         .addClass('input-group-btn')
5804                                                         .append(btn))
5805                                                 .appendTo(s.parent);
5806
5807                                         if (i == focus)
5808                                         {
5809                                                 f.focus();
5810                                         }
5811                                         else if (-(i+1) == focus)
5812                                         {
5813                                                 f.focus();
5814
5815                                                 /* force cursor to end */
5816                                                 var val = f.val();
5817                                                 f.val(' ');
5818                                                 f.val(val);
5819                                         }
5820
5821                                         evdata.input = this.validator(s.sid, f, true);
5822
5823                                         f = null;
5824                                 }
5825
5826                                 evdata = null;
5827                         }
5828
5829                         s = null;
5830                 },
5831
5832                 _keypress: function(ev)
5833                 {
5834                         switch (ev.which)
5835                         {
5836                                 /* backspace, delete */
5837                                 case 8:
5838                                 case 46:
5839                                         if (ev.data.input.val() == '')
5840                                         {
5841                                                 ev.preventDefault();
5842                                                 return false;
5843                                         }
5844
5845                                         return true;
5846
5847                                 /* enter, arrow up, arrow down */
5848                                 case 13:
5849                                 case 38:
5850                                 case 40:
5851                                         ev.preventDefault();
5852                                         return false;
5853                         }
5854
5855                         return true;
5856                 },
5857
5858                 _keydown: function(ev)
5859                 {
5860                         var input = ev.data.input;
5861
5862                         switch (ev.which)
5863                         {
5864                                 /* backspace, delete */
5865                                 case 8:
5866                                 case 46:
5867                                         if (input.val().length == 0)
5868                                         {
5869                                                 ev.preventDefault();
5870
5871                                                 var index = ev.data.index;
5872                                                 var focus = index;
5873
5874                                                 if (ev.which == 8)
5875                                                         focus = -focus;
5876
5877                                                 ev.data.self._redraw(focus, -1, index, ev.data);
5878                                                 return false;
5879                                         }
5880
5881                                         break;
5882
5883                                 /* enter */
5884                                 case 13:
5885                                         ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
5886                                         break;
5887
5888                                 /* arrow up */
5889                                 case 38:
5890                                         var prev = input.parent().prevAll('div.input-group:first').children('input');
5891                                         if (prev.is(':visible'))
5892                                                 prev.focus();
5893                                         else
5894                                                 prev.next('select').focus();
5895                                         break;
5896
5897                                 /* arrow down */
5898                                 case 40:
5899                                         var next = input.parent().nextAll('div.input-group:first').children('input');
5900                                         if (next.is(':visible'))
5901                                                 next.focus();
5902                                         else
5903                                                 next.next('select').focus();
5904                                         break;
5905                         }
5906
5907                         return true;
5908                 },
5909
5910                 _btnclick: function(ev)
5911                 {
5912                         if (!this.getAttribute('disabled'))
5913                         {
5914                                 if (ev.data.remove)
5915                                 {
5916                                         var index = ev.data.index;
5917                                         ev.data.self._redraw(-index, -1, index, ev.data);
5918                                 }
5919                                 else
5920                                 {
5921                                         ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
5922                                 }
5923                         }
5924
5925                         return false;
5926                 },
5927
5928                 widget: function(sid)
5929                 {
5930                         this.options.optional = true;
5931
5932                         var v = this.ucivalue(sid);
5933
5934                         if (!$.isArray(v))
5935                                 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5936
5937                         var d = $('<div />')
5938                                 .attr('id', this.id(sid))
5939                                 .addClass('cbi-input-dynlist');
5940
5941                         this._redraw(NaN, -1, -1, {
5942                                 self:      this,
5943                                 parent:    d[0],
5944                                 values:    v,
5945                                 sid:       sid
5946                         });
5947
5948                         return d;
5949                 },
5950
5951                 ucivalue: function(sid)
5952                 {
5953                         var v = this.callSuper('ucivalue', sid);
5954
5955                         if (!$.isArray(v))
5956                                 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
5957
5958                         return v;
5959                 },
5960
5961                 formvalue: function(sid)
5962                 {
5963                         var rv = [ ];
5964                         var fields = $('#' + this.id(sid) + ' input');
5965
5966                         for (var i = 0; i < fields.length; i++)
5967                                 if (typeof(fields[i].value) == 'string' && fields[i].value.length)
5968                                         rv.push(fields[i].value);
5969
5970                         return rv;
5971                 }
5972         });
5973
5974         this.cbi.DummyValue = this.cbi.AbstractValue.extend({
5975                 widget: function(sid)
5976                 {
5977                         return $('<div />')
5978                                 .addClass('form-control-static')
5979                                 .attr('id', this.id(sid))
5980                                 .html(this.ucivalue(sid));
5981                 },
5982
5983                 formvalue: function(sid)
5984                 {
5985                         return this.ucivalue(sid);
5986                 }
5987         });
5988
5989         this.cbi.ButtonValue = this.cbi.AbstractValue.extend({
5990                 widget: function(sid)
5991                 {
5992                         this.options.optional = true;
5993
5994                         var btn = $('<button />')
5995                                 .addClass('btn btn-default')
5996                                 .attr('id', this.id(sid))
5997                                 .attr('type', 'button')
5998                                 .text(this.label('text'));
5999
6000                         return this.validator(sid, btn);
6001                 }
6002         });
6003
6004         this.cbi.NetworkList = this.cbi.AbstractValue.extend({
6005                 load: function(sid)
6006                 {
6007                         return L.NetworkModel.init();
6008                 },
6009
6010                 _device_icon: function(dev)
6011                 {
6012                         return $('<img />')
6013                                 .attr('src', dev.icon())
6014                                 .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'));
6015                 },
6016
6017                 widget: function(sid)
6018                 {
6019                         var id = this.id(sid);
6020                         var ul = $('<ul />')
6021                                 .attr('id', id)
6022                                 .addClass('list-unstyled');
6023
6024                         var itype = this.options.multiple ? 'checkbox' : 'radio';
6025                         var value = this.ucivalue(sid);
6026                         var check = { };
6027
6028                         if (!this.options.multiple)
6029                                 check[value] = true;
6030                         else
6031                                 for (var i = 0; i < value.length; i++)
6032                                         check[value[i]] = true;
6033
6034                         var interfaces = L.NetworkModel.getInterfaces();
6035
6036                         for (var i = 0; i < interfaces.length; i++)
6037                         {
6038                                 var iface = interfaces[i];
6039
6040                                 $('<li />')
6041                                         .append($('<label />')
6042                                                 .addClass(itype + ' inline')
6043                                                 .append(this.validator(sid, $('<input />')
6044                                                         .attr('name', itype + id)
6045                                                         .attr('type', itype)
6046                                                         .attr('value', iface.name())
6047                                                         .prop('checked', !!check[iface.name()]), true))
6048                                                 .append(iface.renderBadge()))
6049                                         .appendTo(ul);
6050                         }
6051
6052                         if (!this.options.multiple)
6053                         {
6054                                 $('<li />')
6055                                         .append($('<label />')
6056                                                 .addClass(itype + ' inline text-muted')
6057                                                 .append($('<input />')
6058                                                         .attr('name', itype + id)
6059                                                         .attr('type', itype)
6060                                                         .attr('value', '')
6061                                                         .prop('checked', $.isEmptyObject(check)))
6062                                                 .append(L.tr('unspecified')))
6063                                         .appendTo(ul);
6064                         }
6065
6066                         return ul;
6067                 },
6068
6069                 ucivalue: function(sid)
6070                 {
6071                         var v = this.callSuper('ucivalue', sid);
6072
6073                         if (!this.options.multiple)
6074                         {
6075                                 if ($.isArray(v))
6076                                 {
6077                                         return v[0];
6078                                 }
6079                                 else if (typeof(v) == 'string')
6080                                 {
6081                                         v = v.match(/\S+/);
6082                                         return v ? v[0] : undefined;
6083                                 }
6084
6085                                 return v;
6086                         }
6087                         else
6088                         {
6089                                 if (typeof(v) == 'string')
6090                                         v = v.match(/\S+/g);
6091
6092                                 return v || [ ];
6093                         }
6094                 },
6095
6096                 formvalue: function(sid)
6097                 {
6098                         var inputs = $('#' + this.id(sid) + ' input');
6099
6100                         if (!this.options.multiple)
6101                         {
6102                                 for (var i = 0; i < inputs.length; i++)
6103                                         if (inputs[i].checked && inputs[i].value !== '')
6104                                                 return inputs[i].value;
6105
6106                                 return undefined;
6107                         }
6108
6109                         var rv = [ ];
6110
6111                         for (var i = 0; i < inputs.length; i++)
6112                                 if (inputs[i].checked)
6113                                         rv.push(inputs[i].value);
6114
6115                         return rv.length ? rv : undefined;
6116                 }
6117         });
6118
6119         this.cbi.DeviceList = this.cbi.NetworkList.extend({
6120                 _ev_focus: function(ev)
6121                 {
6122                         var self = ev.data.self;
6123                         var input = $(this);
6124
6125                         input.parent().prev().prop('checked', true);
6126                 },
6127
6128                 _ev_blur: function(ev)
6129                 {
6130                         ev.which = 10;
6131                         ev.data.self._ev_keydown.call(this, ev);
6132                 },
6133
6134                 _ev_keydown: function(ev)
6135                 {
6136                         if (ev.which != 10 && ev.which != 13)
6137                                 return;
6138
6139                         var sid = ev.data.sid;
6140                         var self = ev.data.self;
6141                         var input = $(this);
6142                         var ifnames = L.toArray(input.val());
6143
6144                         if (!ifnames.length)
6145                                 return;
6146
6147                         L.NetworkModel.createDevice(ifnames[0]);
6148
6149                         self._redraw(sid, $('#' + self.id(sid)), ifnames[0]);
6150                 },
6151
6152                 load: function(sid)
6153                 {
6154                         return L.NetworkModel.init();
6155                 },
6156
6157                 _redraw: function(sid, ul, sel)
6158                 {
6159                         var id = ul.attr('id');
6160                         var devs = L.NetworkModel.getDevices();
6161                         var iface = L.NetworkModel.getInterface(sid);
6162                         var itype = this.options.multiple ? 'checkbox' : 'radio';
6163                         var check = { };
6164
6165                         if (!sel)
6166                         {
6167                                 for (var i = 0; i < devs.length; i++)
6168                                         if (devs[i].isInNetwork(iface))
6169                                                 check[devs[i].name()] = true;
6170                         }
6171                         else
6172                         {
6173                                 if (this.options.multiple)
6174                                         check = L.toObject(this.formvalue(sid));
6175
6176                                 check[sel] = true;
6177                         }
6178
6179                         ul.empty();
6180
6181                         for (var i = 0; i < devs.length; i++)
6182                         {
6183                                 var dev = devs[i];
6184
6185                                 if (dev.isBridge() && this.options.bridges === false)
6186                                         continue;
6187
6188                                 if (!dev.isBridgeable() && this.options.multiple)
6189                                         continue;
6190
6191                                 var badge = $('<span />')
6192                                         .addClass('badge')
6193                                         .append($('<img />').attr('src', dev.icon()))
6194                                         .append(' %s: %s'.format(dev.name(), dev.description()));
6195
6196                                 //var ifcs = dev.getInterfaces();
6197                                 //if (ifcs.length)
6198                                 //{
6199                                 //      for (var j = 0; j < ifcs.length; j++)
6200                                 //              badge.append((j ? ', ' : ' (') + ifcs[j].name());
6201                                 //
6202                                 //      badge.append(')');
6203                                 //}
6204
6205                                 $('<li />')
6206                                         .append($('<label />')
6207                                                 .addClass(itype + ' inline')
6208                                                 .append($('<input />')
6209                                                         .attr('name', itype + id)
6210                                                         .attr('type', itype)
6211                                                         .attr('value', dev.name())
6212                                                         .prop('checked', !!check[dev.name()]))
6213                                                 .append(badge))
6214                                         .appendTo(ul);
6215                         }
6216
6217
6218                         $('<li />')
6219                                 .append($('<label />')
6220                                         .attr('for', 'custom' + id)
6221                                         .addClass(itype + ' inline')
6222                                         .append($('<input />')
6223                                                 .attr('name', itype + id)
6224                                                 .attr('type', itype)
6225                                                 .attr('value', ''))
6226                                         .append($('<span />')
6227                                                 .addClass('badge')
6228                                                 .append($('<input />')
6229                                                         .attr('id', 'custom' + id)
6230                                                         .attr('type', 'text')
6231                                                         .attr('placeholder', L.tr('Custom device â€¦'))
6232                                                         .on('focus', { self: this, sid: sid }, this._ev_focus)
6233                                                         .on('blur', { self: this, sid: sid }, this._ev_blur)
6234                                                         .on('keydown', { self: this, sid: sid }, this._ev_keydown))))
6235                                 .appendTo(ul);
6236
6237                         if (!this.options.multiple)
6238                         {
6239                                 $('<li />')
6240                                         .append($('<label />')
6241                                                 .addClass(itype + ' inline text-muted')
6242                                                 .append($('<input />')
6243                                                         .attr('name', itype + id)
6244                                                         .attr('type', itype)
6245                                                         .attr('value', '')
6246                                                         .prop('checked', $.isEmptyObject(check)))
6247                                                 .append(L.tr('unspecified')))
6248                                         .appendTo(ul);
6249                         }
6250                 },
6251
6252                 widget: function(sid)
6253                 {
6254                         var id = this.id(sid);
6255                         var ul = $('<ul />')
6256                                 .attr('id', id)
6257                                 .addClass('list-unstyled');
6258
6259                         this._redraw(sid, ul);
6260
6261                         return ul;
6262                 },
6263
6264                 save: function(sid)
6265                 {
6266                         if (this.instance[sid].disabled)
6267                                 return;
6268
6269                         var ifnames = this.formvalue(sid);
6270                         //if (!ifnames)
6271                         //      return;
6272
6273                         var iface = L.NetworkModel.getInterface(sid);
6274                         if (!iface)
6275                                 return;
6276
6277                         iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]);
6278                 }
6279         });
6280
6281
6282         this.cbi.AbstractSection = this.ui.AbstractWidget.extend({
6283                 id: function()
6284                 {
6285                         var s = [ arguments[0], this.map.uci_package, this.uci_type ];
6286
6287                         for (var i = 1; i < arguments.length; i++)
6288                                 s.push(arguments[i].replace(/\./g, '_'));
6289
6290                         return s.join('_');
6291                 },
6292
6293                 option: function(widget, name, options)
6294                 {
6295                         if (this.tabs.length == 0)
6296                                 this.tab({ id: '__default__', selected: true });
6297
6298                         return this.taboption('__default__', widget, name, options);
6299                 },
6300
6301                 tab: function(options)
6302                 {
6303                         if (options.selected)
6304                                 this.tabs.selected = this.tabs.length;
6305
6306                         this.tabs.push({
6307                                 id:          options.id,
6308                                 caption:     options.caption,
6309                                 description: options.description,
6310                                 fields:      [ ],
6311                                 li:          { }
6312                         });
6313                 },
6314
6315                 taboption: function(tabid, widget, name, options)
6316                 {
6317                         var tab;
6318                         for (var i = 0; i < this.tabs.length; i++)
6319                         {
6320                                 if (this.tabs[i].id == tabid)
6321                                 {
6322                                         tab = this.tabs[i];
6323                                         break;
6324                                 }
6325                         }
6326
6327                         if (!tab)
6328                                 throw 'Cannot append to unknown tab ' + tabid;
6329
6330                         var w = widget ? new widget(name, options) : null;
6331
6332                         if (!(w instanceof L.cbi.AbstractValue))
6333                                 throw 'Widget must be an instance of AbstractValue';
6334
6335                         w.section = this;
6336                         w.map     = this.map;
6337
6338                         this.fields[name] = w;
6339                         tab.fields.push(w);
6340
6341                         return w;
6342                 },
6343
6344                 tabtoggle: function(sid)
6345                 {
6346                         for (var i = 0; i < this.tabs.length; i++)
6347                         {
6348                                 var tab = this.tabs[i];
6349                                 var elem = $('#' + this.id('nodetab', sid, tab.id));
6350                                 var empty = true;
6351
6352                                 for (var j = 0; j < tab.fields.length; j++)
6353                                 {
6354                                         if (tab.fields[j].active(sid))
6355                                         {
6356                                                 empty = false;
6357                                                 break;
6358                                         }
6359                                 }
6360
6361                                 if (empty && elem.is(':visible'))
6362                                         elem.fadeOut();
6363                                 else if (!empty)
6364                                         elem.fadeIn();
6365                         }
6366                 },
6367
6368                 ucipackages: function(pkg)
6369                 {
6370                         for (var i = 0; i < this.tabs.length; i++)
6371                                 for (var j = 0; j < this.tabs[i].fields.length; j++)
6372                                         if (this.tabs[i].fields[j].options.uci_package)
6373                                                 pkg[this.tabs[i].fields[j].options.uci_package] = true;
6374                 },
6375
6376                 formvalue: function()
6377                 {
6378                         var rv = { };
6379
6380                         this.sections(function(s) {
6381                                 var sid = s['.name'];
6382                                 var sv = rv[sid] || (rv[sid] = { });
6383
6384                                 for (var i = 0; i < this.tabs.length; i++)
6385                                         for (var j = 0; j < this.tabs[i].fields.length; j++)
6386                                         {
6387                                                 var val = this.tabs[i].fields[j].formvalue(sid);
6388                                                 sv[this.tabs[i].fields[j].name] = val;
6389                                         }
6390                         });
6391
6392                         return rv;
6393                 },
6394
6395                 validate_section: function(sid)
6396                 {
6397                         var inst = this.instance[sid];
6398
6399                         var invals = 0;
6400                         var badge = $('#' + this.id('teaser', sid)).children('span:first');
6401
6402                         for (var i = 0; i < this.tabs.length; i++)
6403                         {
6404                                 var inval = 0;
6405                                 var stbadge = $('#' + this.id('nodetab', sid, this.tabs[i].id)).children('span:first');
6406
6407                                 for (var j = 0; j < this.tabs[i].fields.length; j++)
6408                                         if (!this.tabs[i].fields[j].validate(sid))
6409                                                 inval++;
6410
6411                                 if (inval > 0)
6412                                         stbadge.show()
6413                                                 .text(inval)
6414                                                 .attr('title', L.trp('1 Error', '%d Errors', inval).format(inval));
6415                                 else
6416                                         stbadge.hide();
6417
6418                                 invals += inval;
6419                         }
6420
6421                         if (invals > 0)
6422                                 badge.show()
6423                                         .text(invals)
6424                                         .attr('title', L.trp('1 Error', '%d Errors', invals).format(invals));
6425                         else
6426                                 badge.hide();
6427
6428                         return invals;
6429                 },
6430
6431                 validate: function()
6432                 {
6433                         var errors = 0;
6434                         var as = this.sections();
6435
6436                         for (var i = 0; i < as.length; i++)
6437                         {
6438                                 var invals = this.validate_section(as[i]['.name']);
6439
6440                                 if (invals > 0)
6441                                         errors += invals;
6442                         }
6443
6444                         var badge = $('#' + this.id('sectiontab')).children('span:first');
6445
6446                         if (errors > 0)
6447                                 badge.show()
6448                                         .text(errors)
6449                                         .attr('title', L.trp('1 Error', '%d Errors', errors).format(errors));
6450                         else
6451                                 badge.hide();
6452
6453                         return (errors == 0);
6454                 }
6455         });
6456
6457         this.cbi.TypedSection = this.cbi.AbstractSection.extend({
6458                 init: function(uci_type, options)
6459                 {
6460                         this.uci_type = uci_type;
6461                         this.options  = options;
6462                         this.tabs     = [ ];
6463                         this.fields   = { };
6464                         this.active_panel = 0;
6465                         this.active_tab   = { };
6466                 },
6467
6468                 filter: function(section)
6469                 {
6470                         return true;
6471                 },
6472
6473                 sections: function(cb)
6474                 {
6475                         var s1 = L.uci.sections(this.map.uci_package);
6476                         var s2 = [ ];
6477
6478                         for (var i = 0; i < s1.length; i++)
6479                                 if (s1[i]['.type'] == this.uci_type)
6480                                         if (this.filter(s1[i]))
6481                                                 s2.push(s1[i]);
6482
6483                         if (typeof(cb) == 'function')
6484                                 for (var i = 0; i < s2.length; i++)
6485                                         cb.call(this, s2[i]);
6486
6487                         return s2;
6488                 },
6489
6490                 add: function(name)
6491                 {
6492                         return this.map.add(this.map.uci_package, this.uci_type, name);
6493                 },
6494
6495                 remove: function(sid)
6496                 {
6497                         return this.map.remove(this.map.uci_package, sid);
6498                 },
6499
6500                 _ev_add: function(ev)
6501                 {
6502                         var addb = $(this);
6503                         var name = undefined;
6504                         var self = ev.data.self;
6505
6506                         if (addb.prev().prop('nodeName') == 'INPUT')
6507                                 name = addb.prev().val();
6508
6509                         if (addb.prop('disabled') || name === '')
6510                                 return;
6511
6512                         L.ui.saveScrollTop();
6513
6514                         self.active_panel = -1;
6515                         self.map.save();
6516
6517                         ev.data.sid  = self.add(name);
6518                         ev.data.type = self.uci_type;
6519                         ev.data.name = name;
6520
6521                         self.trigger('add', ev);
6522
6523                         self.map.redraw();
6524
6525                         L.ui.restoreScrollTop();
6526                 },
6527
6528                 _ev_remove: function(ev)
6529                 {
6530                         var self = ev.data.self;
6531                         var sid  = ev.data.sid;
6532
6533                         L.ui.saveScrollTop();
6534
6535                         self.trigger('remove', ev);
6536
6537                         self.map.save();
6538                         self.remove(sid);
6539                         self.map.redraw();
6540
6541                         L.ui.restoreScrollTop();
6542
6543                         ev.stopPropagation();
6544                 },
6545
6546                 _ev_sid: function(ev)
6547                 {
6548                         var self = ev.data.self;
6549                         var text = $(this);
6550                         var addb = text.next();
6551                         var errt = addb.next();
6552                         var name = text.val();
6553
6554                         if (!/^[a-zA-Z0-9_]*$/.test(name))
6555                         {
6556                                 errt.text(L.tr('Invalid section name')).show();
6557                                 text.addClass('error');
6558                                 addb.prop('disabled', true);
6559                                 return false;
6560                         }
6561
6562                         if (L.uci.get(self.map.uci_package, name))
6563                         {
6564                                 errt.text(L.tr('Name already used')).show();
6565                                 text.addClass('error');
6566                                 addb.prop('disabled', true);
6567                                 return false;
6568                         }
6569
6570                         errt.text('').hide();
6571                         text.removeClass('error');
6572                         addb.prop('disabled', false);
6573                         return true;
6574                 },
6575
6576                 _ev_tab: function(ev)
6577                 {
6578                         var self = ev.data.self;
6579                         var sid  = ev.data.sid;
6580
6581                         self.validate();
6582                         self.active_tab[sid] = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
6583                 },
6584
6585                 _ev_panel_collapse: function(ev)
6586                 {
6587                         var self = ev.data.self;
6588
6589                         var this_panel = $(ev.target);
6590                         var this_toggle = this_panel.prevAll('[data-toggle="collapse"]:first');
6591
6592                         var prev_toggle = $($(ev.delegateTarget).find('[data-toggle="collapse"]:eq(%d)'.format(self.active_panel)));
6593                         var prev_panel = $(prev_toggle.attr('data-target'));
6594
6595                         prev_panel
6596                                 .removeClass('in')
6597                                 .addClass('collapse');
6598
6599                         prev_toggle.find('.luci2-section-teaser')
6600                                 .show()
6601                                 .children('span:last')
6602                                 .empty()
6603                                 .append(self.teaser(prev_panel.attr('data-luci2-sid')));
6604
6605                         this_toggle.find('.luci2-section-teaser')
6606                                 .hide();
6607
6608                         self.active_panel = parseInt(this_panel.attr('data-luci2-panel-index'));
6609                         self.validate();
6610                 },
6611
6612                 _ev_panel_open: function(ev)
6613                 {
6614                         var self  = ev.data.self;
6615                         var panel = $($(this).attr('data-target'));
6616                         var index = parseInt(panel.attr('data-luci2-panel-index'));
6617
6618                         if (index == self.active_panel)
6619                                 ev.stopPropagation();
6620                 },
6621
6622                 _ev_sort: function(ev)
6623                 {
6624                         var self    = ev.data.self;
6625                         var cur_idx = ev.data.index;
6626                         var new_idx = cur_idx + (ev.data.up ? -1 : 1);
6627                         var s       = self.sections();
6628
6629                         if (new_idx >= 0 && new_idx < s.length)
6630                         {
6631                                 L.uci.swap(self.map.uci_package, s[cur_idx]['.name'], s[new_idx]['.name']);
6632
6633                                 self.map.save();
6634                                 self.map.redraw();
6635                         }
6636
6637                         ev.stopPropagation();
6638                 },
6639
6640                 teaser: function(sid)
6641                 {
6642                         var tf = this.teaser_fields;
6643
6644                         if (!tf)
6645                         {
6646                                 tf = this.teaser_fields = [ ];
6647
6648                                 if ($.isArray(this.options.teasers))
6649                                 {
6650                                         for (var i = 0; i < this.options.teasers.length; i++)
6651                                         {
6652                                                 var f = this.options.teasers[i];
6653                                                 if (f instanceof L.cbi.AbstractValue)
6654                                                         tf.push(f);
6655                                                 else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue)
6656                                                         tf.push(this.fields[f]);
6657                                         }
6658                                 }
6659                                 else
6660                                 {
6661                                         for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
6662                                                 for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
6663                                                         tf.push(this.tabs[i].fields[j]);
6664                                 }
6665                         }
6666
6667                         var t = '';
6668
6669                         for (var i = 0; i < tf.length; i++)
6670                         {
6671                                 if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
6672                                         continue;
6673
6674                                 var n = tf[i].options.caption || tf[i].name;
6675                                 var v = tf[i].textvalue(sid);
6676
6677                                 if (typeof(v) == 'undefined')
6678                                         continue;
6679
6680                                 t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
6681                         }
6682
6683                         return t;
6684                 },
6685
6686                 _render_add: function()
6687                 {
6688                         if (!this.options.addremove)
6689                                 return null;
6690
6691                         var text = L.tr('Add section');
6692                         var ttip = L.tr('Create new section...');
6693
6694                         if ($.isArray(this.options.add_caption))
6695                                 text = this.options.add_caption[0], ttip = this.options.add_caption[1];
6696                         else if (typeof(this.options.add_caption) == 'string')
6697                                 text = this.options.add_caption, ttip = '';
6698
6699                         var add = $('<div />');
6700
6701                         if (this.options.anonymous === false)
6702                         {
6703                                 $('<input />')
6704                                         .addClass('cbi-input-text')
6705                                         .attr('type', 'text')
6706                                         .attr('placeholder', ttip)
6707                                         .blur({ self: this }, this._ev_sid)
6708                                         .keyup({ self: this }, this._ev_sid)
6709                                         .appendTo(add);
6710
6711                                 $('<img />')
6712                                         .attr('src', L.globals.resource + '/icons/cbi/add.gif')
6713                                         .attr('title', text)
6714                                         .addClass('cbi-button')
6715                                         .click({ self: this }, this._ev_add)
6716                                         .appendTo(add);
6717
6718                                 $('<div />')
6719                                         .addClass('cbi-value-error')
6720                                         .hide()
6721                                         .appendTo(add);
6722                         }
6723                         else
6724                         {
6725                                 L.ui.button(text, 'success', ttip)
6726                                         .click({ self: this }, this._ev_add)
6727                                         .appendTo(add);
6728                         }
6729
6730                         return add;
6731                 },
6732
6733                 _render_remove: function(sid, index)
6734                 {
6735                         if (!this.options.addremove)
6736                                 return null;
6737
6738                         var text = L.tr('Remove');
6739                         var ttip = L.tr('Remove this section');
6740
6741                         if ($.isArray(this.options.remove_caption))
6742                                 text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
6743                         else if (typeof(this.options.remove_caption) == 'string')
6744                                 text = this.options.remove_caption, ttip = '';
6745
6746                         return L.ui.button(text, 'danger', ttip)
6747                                 .click({ self: this, sid: sid, index: index }, this._ev_remove);
6748                 },
6749
6750                 _render_sort: function(sid, index)
6751                 {
6752                         if (!this.options.sortable)
6753                                 return null;
6754
6755                         var b1 = L.ui.button('↑', 'info', L.tr('Move up'))
6756                                 .click({ self: this, index: index, up: true }, this._ev_sort);
6757
6758                         var b2 = L.ui.button('↓', 'info', L.tr('Move down'))
6759                                 .click({ self: this, index: index, up: false }, this._ev_sort);
6760
6761                         return b1.add(b2);
6762                 },
6763
6764                 _render_caption: function()
6765                 {
6766                         return $('<h3 />')
6767                                 .addClass('panel-title')
6768                                 .append(this.label('caption') || this.uci_type);
6769                 },
6770
6771                 _render_description: function()
6772                 {
6773                         var text = this.label('description');
6774
6775                         if (text)
6776                                 return $('<div />')
6777                                         .addClass('luci2-section-description')
6778                                         .text(text);
6779
6780                         return null;
6781                 },
6782
6783                 _render_teaser: function(sid, index)
6784                 {
6785                         if (this.options.collabsible || this.map.options.collabsible)
6786                         {
6787                                 return $('<div />')
6788                                         .attr('id', this.id('teaser', sid))
6789                                         .addClass('luci2-section-teaser well well-sm')
6790                                         .append($('<span />')
6791                                                 .addClass('badge'))
6792                                         .append($('<span />'));
6793                         }
6794
6795                         return null;
6796                 },
6797
6798                 _render_head: function(condensed)
6799                 {
6800                         if (condensed)
6801                                 return null;
6802
6803                         return $('<div />')
6804                                 .addClass('panel-heading')
6805                                 .append(this._render_caption())
6806                                 .append(this._render_description());
6807                 },
6808
6809                 _render_tab_description: function(sid, index, tab_index)
6810                 {
6811                         var tab = this.tabs[tab_index];
6812
6813                         if (typeof(tab.description) == 'string')
6814                         {
6815                                 return $('<div />')
6816                                         .addClass('cbi-tab-descr')
6817                                         .text(tab.description);
6818                         }
6819
6820                         return null;
6821                 },
6822
6823                 _render_tab_head: function(sid, index, tab_index)
6824                 {
6825                         var tab = this.tabs[tab_index];
6826                         var cur = this.active_tab[sid] || 0;
6827
6828                         var tabh = $('<li />')
6829                                 .append($('<a />')
6830                                         .attr('id', this.id('nodetab', sid, tab.id))
6831                                         .attr('href', '#' + this.id('node', sid, tab.id))
6832                                         .attr('data-toggle', 'tab')
6833                                         .attr('data-luci2-tab-index', tab_index)
6834                                         .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ')
6835                                         .append($('<span />')
6836                                                 .addClass('badge'))
6837                                         .on('shown.bs.tab', { self: this, sid: sid }, this._ev_tab));
6838
6839                         if (cur == tab_index)
6840                                 tabh.addClass('active');
6841
6842                         if (!tab.fields.length)
6843                                 tabh.hide();
6844
6845                         return tabh;
6846                 },
6847
6848                 _render_tab_body: function(sid, index, tab_index)
6849                 {
6850                         var tab = this.tabs[tab_index];
6851                         var cur = this.active_tab[sid] || 0;
6852
6853                         var tabb = $('<div />')
6854                                 .addClass('tab-pane')
6855                                 .attr('id', this.id('node', sid, tab.id))
6856                                 .attr('data-luci2-tab-index', tab_index)
6857                                 .append(this._render_tab_description(sid, index, tab_index));
6858
6859                         if (cur == tab_index)
6860                                 tabb.addClass('active');
6861
6862                         for (var i = 0; i < tab.fields.length; i++)
6863                                 tabb.append(tab.fields[i].render(sid));
6864
6865                         return tabb;
6866                 },
6867
6868                 _render_section_head: function(sid, index)
6869                 {
6870                         var head = $('<div />')
6871                                 .addClass('luci2-section-header')
6872                                 .append(this._render_teaser(sid, index))
6873                                 .append($('<div />')
6874                                         .addClass('btn-group')
6875                                         .append(this._render_sort(sid, index))
6876                                         .append(this._render_remove(sid, index)));
6877
6878                         if (this.options.collabsible)
6879                         {
6880                                 head.attr('data-toggle', 'collapse')
6881                                         .attr('data-parent', this.id('sectiongroup'))
6882                                         .attr('data-target', '#' + this.id('panel', sid))
6883                                         .on('click', { self: this }, this._ev_panel_open);
6884                         }
6885
6886                         return head;
6887                 },
6888
6889                 _render_section_body: function(sid, index)
6890                 {
6891                         var body = $('<div />')
6892                                 .attr('id', this.id('panel', sid))
6893                                 .attr('data-luci2-panel-index', index)
6894                                 .attr('data-luci2-sid', sid);
6895
6896                         if (this.options.collabsible || this.map.options.collabsible)
6897                         {
6898                                 body.addClass('panel-collapse collapse');
6899
6900                                 if (index == this.active_panel)
6901                                         body.addClass('in');
6902                         }
6903
6904                         var tab_heads = $('<ul />')
6905                                 .addClass('nav nav-tabs');
6906
6907                         var tab_bodies = $('<div />')
6908                                 .addClass('form-horizontal tab-content')
6909                                 .append(tab_heads);
6910
6911                         for (var j = 0; j < this.tabs.length; j++)
6912                         {
6913                                 tab_heads.append(this._render_tab_head(sid, index, j));
6914                                 tab_bodies.append(this._render_tab_body(sid, index, j));
6915                         }
6916
6917                         body.append(tab_bodies);
6918
6919                         if (this.tabs.length <= 1)
6920                                 tab_heads.hide();
6921
6922                         return body;
6923                 },
6924
6925                 _render_body: function(condensed)
6926                 {
6927                         var s = this.sections();
6928
6929                         if (this.active_panel < 0)
6930                                 this.active_panel += s.length;
6931                         else if (this.active_panel >= s.length)
6932                                 this.active_panel = s.length - 1;
6933
6934                         var body = $('<ul />')
6935                                 .addClass('list-group');
6936
6937                         if (this.options.collabsible)
6938                         {
6939                                 body.attr('id', this.id('sectiongroup'))
6940                                         .on('show.bs.collapse', { self: this }, this._ev_panel_collapse);
6941                         }
6942
6943                         if (s.length == 0)
6944                         {
6945                                 body.append($('<li />')
6946                                         .addClass('list-group-item text-muted')
6947                                         .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))
6948                         }
6949
6950                         for (var i = 0; i < s.length; i++)
6951                         {
6952                                 var sid = s[i]['.name'];
6953                                 var inst = this.instance[sid] = { tabs: [ ] };
6954
6955                                 body.append($('<li />')
6956                                         .addClass('list-group-item')
6957                                         .append(this._render_section_head(sid, i))
6958                                         .append(this._render_section_body(sid, i)));
6959                         }
6960
6961                         return body;
6962                 },
6963
6964                 render: function(condensed)
6965                 {
6966                         this.instance = { };
6967
6968                         var panel = $('<div />')
6969                                 .addClass('panel panel-default')
6970                                 .append(this._render_head(condensed))
6971                                 .append(this._render_body(condensed));
6972
6973                         if (this.options.addremove)
6974                                 panel.append($('<div />')
6975                                         .addClass('panel-footer')
6976                                         .append(this._render_add()));
6977
6978                         return panel;
6979                 },
6980
6981                 finish: function()
6982                 {
6983                         var s = this.sections();
6984
6985                         for (var i = 0; i < s.length; i++)
6986                         {
6987                                 var sid = s[i]['.name'];
6988
6989                                 this.validate_section(sid);
6990
6991                                 if (i != this.active_panel)
6992                                         $('#' + this.id('teaser', sid)).children('span:last')
6993                                                 .append(this.teaser(sid));
6994                                 else
6995                                         $('#' + this.id('teaser', sid))
6996                                                 .hide();
6997                         }
6998                 }
6999         });
7000
7001         this.cbi.TableSection = this.cbi.TypedSection.extend({
7002                 _render_table_head: function()
7003                 {
7004                         var thead = $('<thead />')
7005                                 .append($('<tr />')
7006                                         .addClass('cbi-section-table-titles'));
7007
7008                         for (var j = 0; j < this.tabs[0].fields.length; j++)
7009                                 thead.children().append($('<th />')
7010                                         .addClass('cbi-section-table-cell')
7011                                         .css('width', this.tabs[0].fields[j].options.width || '')
7012                                         .append(this.tabs[0].fields[j].label('caption')));
7013
7014                         if (this.options.addremove !== false || this.options.sortable)
7015                                 thead.children().append($('<th />')
7016                                         .addClass('cbi-section-table-cell')
7017                                         .text(' '));
7018
7019                         return thead;
7020                 },
7021
7022                 _render_table_row: function(sid, index)
7023                 {
7024                         var row = $('<tr />')
7025                                 .attr('data-luci2-sid', sid);
7026
7027                         for (var j = 0; j < this.tabs[0].fields.length; j++)
7028                         {
7029                                 row.append($('<td />')
7030                                         .css('width', this.tabs[0].fields[j].options.width || '')
7031                                         .append(this.tabs[0].fields[j].render(sid, true)));
7032                         }
7033
7034                         if (this.options.addremove !== false || this.options.sortable)
7035                         {
7036                                 row.append($('<td />')
7037                                         .addClass('text-right')
7038                                         .append($('<div />')
7039                                                 .addClass('btn-group')
7040                                                 .append(this._render_sort(sid, index))
7041                                                 .append(this._render_remove(sid, index))));
7042                         }
7043
7044                         return row;
7045                 },
7046
7047                 _render_table_body: function()
7048                 {
7049                         var s = this.sections();
7050
7051                         var tbody = $('<tbody />');
7052
7053                         if (s.length == 0)
7054                         {
7055                                 var cols = this.tabs[0].fields.length;
7056
7057                                 if (this.options.addremove !== false || this.options.sortable)
7058                                         cols++;
7059
7060                                 tbody.append($('<tr />')
7061                                         .append($('<td />')
7062                                                 .addClass('text-muted')
7063                                                 .attr('colspan', cols)
7064                                                 .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
7065                         }
7066
7067                         for (var i = 0; i < s.length; i++)
7068                         {
7069                                 var sid = s[i]['.name'];
7070                                 var inst = this.instance[sid] = { tabs: [ ] };
7071
7072                                 tbody.append(this._render_table_row(sid, i));
7073                         }
7074
7075                         return tbody;
7076                 },
7077
7078                 _render_body: function(condensed)
7079                 {
7080                         return $('<table />')
7081                                 .addClass('table table-condensed table-hover')
7082                                 .append(this._render_table_head())
7083                                 .append(this._render_table_body());
7084                 }
7085         });
7086
7087         this.cbi.NamedSection = this.cbi.TypedSection.extend({
7088                 sections: function(cb)
7089                 {
7090                         var sa = [ ];
7091                         var sl = L.uci.sections(this.map.uci_package);
7092
7093                         for (var i = 0; i < sl.length; i++)
7094                                 if (sl[i]['.name'] == this.uci_type)
7095                                 {
7096                                         sa.push(sl[i]);
7097                                         break;
7098                                 }
7099
7100                         if (typeof(cb) == 'function' && sa.length > 0)
7101                                 cb.call(this, sa[0]);
7102
7103                         return sa;
7104                 }
7105         });
7106
7107         this.cbi.SingleSection = this.cbi.NamedSection.extend({
7108                 render: function()
7109                 {
7110                         this.instance = { };
7111                         this.instance[this.uci_type] = { tabs: [ ] };
7112
7113                         return this._render_section_body(this.uci_type, 0);
7114                 }
7115         });
7116
7117         this.cbi.DummySection = this.cbi.TypedSection.extend({
7118                 sections: function(cb)
7119                 {
7120                         if (typeof(cb) == 'function')
7121                                 cb.apply(this, [ { '.name': this.uci_type } ]);
7122
7123                         return [ { '.name': this.uci_type } ];
7124                 }
7125         });
7126
7127         this.cbi.Map = this.ui.AbstractWidget.extend({
7128                 init: function(uci_package, options)
7129                 {
7130                         var self = this;
7131
7132                         this.uci_package = uci_package;
7133                         this.sections = [ ];
7134                         this.options = L.defaults(options, {
7135                                 save:    function() { },
7136                                 prepare: function() { }
7137                         });
7138                 },
7139
7140                 _load_cb: function()
7141                 {
7142                         var deferreds = [ L.deferrable(this.options.prepare()) ];
7143
7144                         for (var i = 0; i < this.sections.length; i++)
7145                         {
7146                                 for (var f in this.sections[i].fields)
7147                                 {
7148                                         if (typeof(this.sections[i].fields[f].load) != 'function')
7149                                                 continue;
7150
7151                                         var s = this.sections[i].sections();
7152                                         for (var j = 0; j < s.length; j++)
7153                                         {
7154                                                 var rv = this.sections[i].fields[f].load(s[j]['.name']);
7155                                                 if (L.isDeferred(rv))
7156                                                         deferreds.push(rv);
7157                                         }
7158                                 }
7159                         }
7160
7161                         return $.when.apply($, deferreds);
7162                 },
7163
7164                 load: function()
7165                 {
7166                         var self = this;
7167                         var packages = { };
7168
7169                         for (var i = 0; i < this.sections.length; i++)
7170                                 this.sections[i].ucipackages(packages);
7171
7172                         packages[this.uci_package] = true;
7173
7174                         for (var pkg in packages)
7175                                 if (!L.uci.writable(pkg))
7176                                         this.options.readonly = true;
7177
7178                         return L.uci.load(L.toArray(packages)).then(function() {
7179                                 return self._load_cb();
7180                         });
7181                 },
7182
7183                 _ev_tab: function(ev)
7184                 {
7185                         var self = ev.data.self;
7186
7187                         self.validate();
7188                         self.active_tab = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
7189                 },
7190
7191                 _ev_apply: function(ev)
7192                 {
7193                         var self = ev.data.self;
7194
7195                         self.trigger('apply', ev);
7196                 },
7197
7198                 _ev_save: function(ev)
7199                 {
7200                         var self = ev.data.self;
7201
7202                         self.send().then(function() {
7203                                 self.trigger('save', ev);
7204                         });
7205                 },
7206
7207                 _ev_reset: function(ev)
7208                 {
7209                         var self = ev.data.self;
7210
7211                         self.trigger('reset', ev);
7212                         self.reset();
7213                 },
7214
7215                 _render_tab_head: function(tab_index)
7216                 {
7217                         var section = this.sections[tab_index];
7218                         var cur = this.active_tab || 0;
7219
7220                         var tabh = $('<li />')
7221                                 .append($('<a />')
7222                                         .attr('id', section.id('sectiontab'))
7223                                         .attr('href', '#' + section.id('section'))
7224                                         .attr('data-toggle', 'tab')
7225                                         .attr('data-luci2-tab-index', tab_index)
7226                                         .text(section.label('caption') + ' ')
7227                                         .append($('<span />')
7228                                                 .addClass('badge'))
7229                                         .on('shown.bs.tab', { self: this }, this._ev_tab));
7230
7231                         if (cur == tab_index)
7232                                 tabh.addClass('active');
7233
7234                         return tabh;
7235                 },
7236
7237                 _render_tab_body: function(tab_index)
7238                 {
7239                         var section = this.sections[tab_index];
7240                         var desc = section.label('description');
7241                         var cur = this.active_tab || 0;
7242
7243                         var tabb = $('<div />')
7244                                 .addClass('tab-pane')
7245                                 .attr('id', section.id('section'))
7246                                 .attr('data-luci2-tab-index', tab_index);
7247
7248                         if (cur == tab_index)
7249                                 tabb.addClass('active');
7250
7251                         if (desc)
7252                                 tabb.append($('<p />')
7253                                         .text(desc));
7254
7255                         var s = section.render(this.options.tabbed);
7256
7257                         if (this.options.readonly || section.options.readonly)
7258                                 s.find('input, select, button, img.cbi-button').attr('disabled', true);
7259
7260                         tabb.append(s);
7261
7262                         return tabb;
7263                 },
7264
7265                 _render_body: function()
7266                 {
7267                         var tabs = $('<ul />')
7268                                 .addClass('nav nav-tabs');
7269
7270                         var body = $('<div />')
7271                                 .append(tabs);
7272
7273                         for (var i = 0; i < this.sections.length; i++)
7274                         {
7275                                 tabs.append(this._render_tab_head(i));
7276                                 body.append(this._render_tab_body(i));
7277                         }
7278
7279                         if (this.options.tabbed)
7280                                 body.addClass('tab-content');
7281                         else
7282                                 tabs.hide();
7283
7284                         return body;
7285                 },
7286
7287                 _render_footer: function()
7288                 {
7289                         var evdata = {
7290                                 self: this
7291                         };
7292
7293                         return $('<div />')
7294                                 .addClass('panel panel-default panel-body text-right')
7295                                 .append($('<div />')
7296                                         .addClass('btn-group')
7297                                         .append(L.ui.button(L.tr('Save & Apply'), 'primary')
7298                                                 .click(evdata, this._ev_apply))
7299                                         .append(L.ui.button(L.tr('Save'), 'default')
7300                                                 .click(evdata, this._ev_save))
7301                                         .append(L.ui.button(L.tr('Reset'), 'default')
7302                                                 .click(evdata, this._ev_reset)));
7303                 },
7304
7305                 render: function()
7306                 {
7307                         var map = $('<form />');
7308
7309                         if (typeof(this.options.caption) == 'string')
7310                                 map.append($('<h2 />')
7311                                         .text(this.options.caption));
7312
7313                         if (typeof(this.options.description) == 'string')
7314                                 map.append($('<p />')
7315                                         .text(this.options.description));
7316
7317                         map.append(this._render_body());
7318
7319                         if (this.options.pageaction !== false)
7320                                 map.append(this._render_footer());
7321
7322                         return map;
7323                 },
7324
7325                 finish: function()
7326                 {
7327                         for (var i = 0; i < this.sections.length; i++)
7328                                 this.sections[i].finish();
7329
7330                         this.validate();
7331                 },
7332
7333                 redraw: function()
7334                 {
7335                         this.target.hide().empty().append(this.render());
7336                         this.finish();
7337                         this.target.show();
7338                 },
7339
7340                 section: function(widget, uci_type, options)
7341                 {
7342                         var w = widget ? new widget(uci_type, options) : null;
7343
7344                         if (!(w instanceof L.cbi.AbstractSection))
7345                                 throw 'Widget must be an instance of AbstractSection';
7346
7347                         w.map = this;
7348                         w.index = this.sections.length;
7349
7350                         this.sections.push(w);
7351                         return w;
7352                 },
7353
7354                 formvalue: function()
7355                 {
7356                         var rv = { };
7357
7358                         for (var i = 0; i < this.sections.length; i++)
7359                         {
7360                                 var sids = this.sections[i].formvalue();
7361                                 for (var sid in sids)
7362                                 {
7363                                         var s = rv[sid] || (rv[sid] = { });
7364                                         $.extend(s, sids[sid]);
7365                                 }
7366                         }
7367
7368                         return rv;
7369                 },
7370
7371                 add: function(conf, type, name)
7372                 {
7373                         return L.uci.add(conf, type, name);
7374                 },
7375
7376                 remove: function(conf, sid)
7377                 {
7378                         return L.uci.remove(conf, sid);
7379                 },
7380
7381                 get: function(conf, sid, opt)
7382                 {
7383                         return L.uci.get(conf, sid, opt);
7384                 },
7385
7386                 set: function(conf, sid, opt, val)
7387                 {
7388                         return L.uci.set(conf, sid, opt, val);
7389                 },
7390
7391                 validate: function()
7392                 {
7393                         var rv = true;
7394
7395                         for (var i = 0; i < this.sections.length; i++)
7396                         {
7397                                 if (!this.sections[i].validate())
7398                                         rv = false;
7399                         }
7400
7401                         return rv;
7402                 },
7403
7404                 save: function()
7405                 {
7406                         var self = this;
7407
7408                         if (self.options.readonly)
7409                                 return L.deferrable();
7410
7411                         var deferreds = [ ];
7412
7413                         for (var i = 0; i < self.sections.length; i++)
7414                         {
7415                                 if (self.sections[i].options.readonly)
7416                                         continue;
7417
7418                                 for (var f in self.sections[i].fields)
7419                                 {
7420                                         if (typeof(self.sections[i].fields[f].save) != 'function')
7421                                                 continue;
7422
7423                                         var s = self.sections[i].sections();
7424                                         for (var j = 0; j < s.length; j++)
7425                                         {
7426                                                 var rv = self.sections[i].fields[f].save(s[j]['.name']);
7427                                                 if (L.isDeferred(rv))
7428                                                         deferreds.push(rv);
7429                                         }
7430                                 }
7431                         }
7432
7433                         return $.when.apply($, deferreds).then(function() {
7434                                 return L.deferrable(self.options.save());
7435                         });
7436                 },
7437
7438                 send: function()
7439                 {
7440                         if (!this.validate())
7441                                 return L.deferrable();
7442
7443                         var self = this;
7444
7445                         L.ui.saveScrollTop();
7446                         L.ui.loading(true);
7447
7448                         return this.save().then(function() {
7449                                 return L.uci.save();
7450                         }).then(function() {
7451                                 return L.ui.updateChanges();
7452                         }).then(function() {
7453                                 return self.load();
7454                         }).then(function() {
7455                                 self.redraw();
7456                                 self = null;
7457
7458                                 L.ui.loading(false);
7459                                 L.ui.restoreScrollTop();
7460                         });
7461                 },
7462
7463                 revert: function()
7464                 {
7465                         var packages = { };
7466
7467                         for (var i = 0; i < this.sections.length; i++)
7468                                 this.sections[i].ucipackages(packages);
7469
7470                         packages[this.uci_package] = true;
7471
7472                         L.uci.unload(L.toArray(packages));
7473                 },
7474
7475                 reset: function()
7476                 {
7477                         var self = this;
7478
7479                         self.revert();
7480
7481                         return self.insertInto(self.target);
7482                 },
7483
7484                 insertInto: function(id)
7485                 {
7486                         var self = this;
7487                             self.target = $(id);
7488
7489                         L.ui.loading(true);
7490                         self.target.hide();
7491
7492                         return self.load().then(function() {
7493                                 self.target.empty().append(self.render());
7494                                 self.finish();
7495                                 self.target.show();
7496                                 self = null;
7497                                 L.ui.loading(false);
7498                         });
7499                 }
7500         });
7501
7502         this.cbi.Modal = this.cbi.Map.extend({
7503                 _ev_apply: function(ev)
7504                 {
7505                         var self = ev.data.self;
7506
7507                         self.trigger('apply', ev);
7508                 },
7509
7510                 _ev_save: function(ev)
7511                 {
7512                         var self = ev.data.self;
7513
7514                         self.send().then(function() {
7515                                 self.trigger('save', ev);
7516                                 self.close();
7517                         });
7518                 },
7519
7520                 _ev_reset: function(ev)
7521                 {
7522                         var self = ev.data.self;
7523
7524                         self.trigger('close', ev);
7525                         self.revert();
7526                         self.close();
7527                 },
7528
7529                 _render_footer: function()
7530                 {
7531                         var evdata = {
7532                                 self: this
7533                         };
7534
7535                         return $('<div />')
7536                                 .addClass('btn-group')
7537                                 .append(L.ui.button(L.tr('Save & Apply'), 'primary')
7538                                         .click(evdata, this._ev_apply))
7539                                 .append(L.ui.button(L.tr('Save'), 'default')
7540                                         .click(evdata, this._ev_save))
7541                                 .append(L.ui.button(L.tr('Cancel'), 'default')
7542                                         .click(evdata, this._ev_reset));
7543                 },
7544
7545                 render: function()
7546                 {
7547                         var modal = L.ui.dialog(this.label('caption'), null, { wide: true });
7548                         var map = $('<form />');
7549
7550                         var desc = this.label('description');
7551                         if (desc)
7552                                 map.append($('<p />').text(desc));
7553
7554                         map.append(this._render_body());
7555
7556                         modal.find('.modal-body').append(map);
7557                         modal.find('.modal-footer').append(this._render_footer());
7558
7559                         return modal;
7560                 },
7561
7562                 redraw: function()
7563                 {
7564                         this.render();
7565                         this.finish();
7566                 },
7567
7568                 show: function()
7569                 {
7570                         var self = this;
7571
7572                         L.ui.loading(true);
7573
7574                         return self.load().then(function() {
7575                                 self.render();
7576                                 self.finish();
7577
7578                                 L.ui.loading(false);
7579                         });
7580                 },
7581
7582                 close: function()
7583                 {
7584                         L.ui.dialog(false);
7585                 }
7586         });
7587 };