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