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