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