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