Initial commit of LuCI2
[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 _luci2 = this;
179
180 var alphacmp = function(a, b)
181 {
182 if (a < b)
183 return -1;
184 else if (a > b)
185 return 1;
186 else
187 return 0;
188 };
189
190 var retcb = function(cb, rv)
191 {
192 if (typeof(cb) == 'function')
193 cb(rv);
194
195 return rv;
196 };
197
198 var isa = function(x, t)
199 {
200 if (typeof(x) != 'string' && typeof(t) == 'string')
201 return (Object.prototype.toString.call(x) == '[object ' + t + ']');
202
203 return (Object.prototype.toString.call(x) == Object.prototype.toString.call(t));
204 };
205
206 var rcall = function(obj, func, params, res_attr, res_default, cb, filter)
207 {
208 if (typeof(params) == 'undefined')
209 params = { };
210
211 return _luci2.rpc.call(obj, func, params).then(function(res) {
212 if (res[0] != 0 || typeof(res[1]) == 'undefined')
213 return retcb(cb, res_default);
214
215 var rv = (typeof(res_attr) != 'undefined') ? res[1][res_attr] : res[1];
216 if (typeof(rv) == 'undefined' || (typeof(res_default) != 'undefined' && !isa(rv, res_default)))
217 return retcb(cb, res_default);
218
219 if (typeof(filter) == 'function')
220 rv = filter(rv);
221
222 return retcb(cb, rv);
223 });
224 };
225
226 var Class = function() { };
227
228 Class.extend = function(properties)
229 {
230 Class.initializing = true;
231
232 var prototype = new this();
233 var superprot = this.prototype;
234
235 Class.initializing = false;
236
237 $.extend(prototype, properties, {
238 callSuper: function() {
239 var args = [ ];
240 var meth = arguments[0];
241
242 if (typeof(superprot[meth]) != 'function')
243 return undefined;
244
245 for (var i = 1; i < arguments.length; i++)
246 args.push(arguments[i]);
247
248 return superprot[meth].apply(this, args);
249 }
250 });
251
252 function _class()
253 {
254 this.options = arguments[0] || { };
255
256 if (!Class.initializing && typeof(this.init) == 'function')
257 this.init.apply(this, arguments);
258 }
259
260 _class.prototype = prototype;
261 _class.prototype.constructor = _class;
262
263 _class.extend = arguments.callee;
264
265 return _class;
266 };
267
268 this.defaults = function(obj, def)
269 {
270 for (var key in def)
271 if (typeof(obj[key]) == 'undefined')
272 obj[key] = def[key];
273
274 return obj;
275 };
276
277 this.deferred = function(x)
278 {
279 return (typeof(x) == 'object' &&
280 typeof(x.then) == 'function' &&
281 typeof(x.promise) == 'function');
282 };
283
284 this.deferrable = function()
285 {
286 if (this.deferred(arguments[0]))
287 return arguments[0];
288
289 var d = $.Deferred();
290 d.resolve.apply(d, arguments);
291
292 return d.promise();
293 };
294
295 this.i18n = {
296
297 loaded: false,
298 catalog: { },
299 plural: function(n) { return 0 + (n != 1) },
300
301 init: function() {
302 if (_luci2.i18n.loaded)
303 return;
304
305 var lang = (navigator.userLanguage || navigator.language || 'en').toLowerCase();
306 var langs = (lang.indexOf('-') > -1) ? [ lang, lang.split(/-/)[0] ] : [ lang ];
307
308 for (var i = 0; i < langs.length; i++)
309 $.ajax('%s/i18n/base.%s.json'.format(_luci2.globals.resource, langs[i]), {
310 async: false,
311 cache: true,
312 dataType: 'json',
313 success: function(data) {
314 $.extend(_luci2.i18n.catalog, data);
315
316 var pe = _luci2.i18n.catalog[''];
317 if (pe)
318 {
319 delete _luci2.i18n.catalog[''];
320 try {
321 var pf = new Function('n', 'return 0 + (' + pe + ')');
322 _luci2.i18n.plural = pf;
323 } catch (e) { };
324 }
325 }
326 });
327
328 _luci2.i18n.loaded = true;
329 }
330
331 };
332
333 this.tr = function(msgid)
334 {
335 _luci2.i18n.init();
336
337 var msgstr = _luci2.i18n.catalog[msgid];
338
339 if (typeof(msgstr) == 'undefined')
340 return msgid;
341 else if (typeof(msgstr) == 'string')
342 return msgstr;
343 else
344 return msgstr[0];
345 };
346
347 this.trp = function(msgid, msgid_plural, count)
348 {
349 _luci2.i18n.init();
350
351 var msgstr = _luci2.i18n.catalog[msgid];
352
353 if (typeof(msgstr) == 'undefined')
354 return (count == 1) ? msgid : msgid_plural;
355 else if (typeof(msgstr) == 'string')
356 return msgstr;
357 else
358 return msgstr[_luci2.i18n.plural(count)];
359 };
360
361 this.trc = function(msgctx, msgid)
362 {
363 _luci2.i18n.init();
364
365 var msgstr = _luci2.i18n.catalog[msgid + '\u0004' + msgctx];
366
367 if (typeof(msgstr) == 'undefined')
368 return msgid;
369 else if (typeof(msgstr) == 'string')
370 return msgstr;
371 else
372 return msgstr[0];
373 };
374
375 this.trcp = function(msgctx, msgid, msgid_plural, count)
376 {
377 _luci2.i18n.init();
378
379 var msgstr = _luci2.i18n.catalog[msgid + '\u0004' + msgctx];
380
381 if (typeof(msgstr) == 'undefined')
382 return (count == 1) ? msgid : msgid_plural;
383 else if (typeof(msgstr) == 'string')
384 return msgstr;
385 else
386 return msgstr[_luci2.i18n.plural(count)];
387 };
388
389 this.setHash = function(key, value)
390 {
391 var h = '';
392 var data = this.getHash(undefined);
393
394 if (typeof(value) == 'undefined')
395 delete data[key];
396 else
397 data[key] = value;
398
399 var keys = [ ];
400 for (var k in data)
401 keys.push(k);
402
403 keys.sort();
404
405 for (var i = 0; i < keys.length; i++)
406 {
407 if (i > 0)
408 h += ',';
409
410 h += keys[i] + ':' + data[keys[i]];
411 }
412
413 if (h)
414 location.hash = '#' + h;
415 };
416
417 this.getHash = function(key)
418 {
419 var data = { };
420 var tuples = (location.hash || '#').substring(1).split(/,/);
421
422 for (var i = 0; i < tuples.length; i++)
423 {
424 var tuple = tuples[i].split(/:/);
425 if (tuple.length == 2)
426 data[tuple[0]] = tuple[1];
427 }
428
429 if (typeof(key) != 'undefined')
430 return data[key];
431
432 return data;
433 };
434
435 this.globals = {
436 resource: '/luci2'
437 };
438
439 this.rpc = {
440
441 _msg_id: 1,
442
443 _wrap_msg: function(method, object, func, args)
444 {
445 if (typeof(args) != 'object')
446 args = { };
447
448 return {
449 id: _luci2.rpc._msg_id++,
450 jsonrpc: "2.0",
451 method: method,
452 params: (method == 'call') ? [ _luci2.globals.sid, object, func, args ] : object
453 };
454 },
455
456 _parse_response: function(keys, priv)
457 {
458 return function(data) {
459 var obj;
460 try {
461 obj = $.parseJSON(data);
462 } catch(e) { }
463
464 if (typeof(obj) != 'object')
465 return undefined;
466
467 /* is a batched response */
468 if (keys)
469 {
470 var rv = { };
471 for (var i = 0; i < obj.length; i++)
472 {
473 var p = (typeof(priv) != 'undefined') ? priv[i] : undefined;
474
475 if ($.isArray(obj[i].result) && typeof(priv) != 'undefined')
476 obj[i].result[2] = p;
477
478 if (obj[i].jsonrpc != '2.0' || obj[i].error || !obj[i].result)
479 rv[keys[i]] = [ 4 /* UBUS_STATUS_NO_DATA */, undefined, p ];
480 else
481 rv[keys[i]] = obj[i].result;
482 }
483 return rv;
484 }
485
486 if (obj.jsonrpc != '2.0' || obj.error || !obj.result)
487 return [ 4 /* UBUS_STATUS_NO_DATA */, undefined, priv ];
488
489 if ($.isArray(obj.result) && typeof(priv) != 'undefined')
490 obj.result[2] = priv;
491
492 return obj.result;
493 };
494 },
495
496 _post_msg: function(message, cb, keys, priv)
497 {
498 return $.ajax('/ubus', {
499 cache: false,
500 contentType: 'application/json',
501 data: JSON.stringify(message),
502 dataFilter: _luci2.rpc._parse_response(keys, priv),
503 dataType: 'text',
504 success: cb,
505 type: 'POST'
506 });
507 },
508
509 _post_single: function(object, method, args, cb, priv)
510 {
511 var msg = _luci2.rpc._wrap_msg('call', object, method, args, priv);
512 return _luci2.rpc._post_msg(msg, cb, undefined, priv);
513 },
514
515 _post_batch: function(methods, cb)
516 {
517 if (typeof(methods) != 'object')
518 return undefined;
519
520 var msgs = [ ];
521 var keys = [ ];
522 var priv = [ ];
523
524 for (var k in methods)
525 {
526 if (typeof(methods[k]) != 'object' || methods[k].length < 2)
527 continue;
528
529 keys.push(k);
530 priv.push(methods[k][3]);
531 msgs.push(_luci2.rpc._wrap_msg('call', methods[k][0], methods[k][1], methods[k][2]));
532 }
533
534 if (msgs.length > 0)
535 return _luci2.rpc._post_msg(msgs, cb, keys, priv);
536
537 return _luci2.deferrable([ ]);
538 },
539
540 call: function()
541 {
542 var a = arguments;
543 if (typeof a[0] == 'string')
544 return _luci2.rpc._post_single(a[0], a[1], a[2], a[3], a[4]);
545 else
546 return _luci2.rpc._post_batch(a[0], a[1]);
547 },
548
549 list: function(objects)
550 {
551 var msg = _luci2.rpc._wrap_msg('list', objects);
552 return _luci2.rpc._post_msg(msg);
553 },
554
555 access: function(scope, object, method, cb)
556 {
557 return _luci2.rpc._post_single('session', 'access', {
558 'sid': _luci2.globals.sid,
559 'scope': scope,
560 'object': object,
561 'function': method
562 }, function(rv) {
563 return retcb(cb, (rv[0] == 0 && rv[1] && rv[1].access == true));
564 });
565 }
566 };
567
568 this.uci = {
569
570 writable: function(cb)
571 {
572 return _luci2.rpc.access('ubus', 'uci', 'commit', cb);
573 },
574
575 add: function(config, type, cb)
576 {
577 return rcall('uci', 'add', { config: config, type: type }, 'section', '', cb);
578 },
579
580 apply: function()
581 {
582
583 },
584
585 changes: function(config)
586 {
587 return rcall('uci', 'changes', { config: config }, 'changes', [ ], cb);
588 },
589
590 commit: function(config)
591 {
592 return rcall('uci', 'commit', { config: config }, undefined, undefined, cb);
593 },
594
595 'delete': function(config, section, option)
596 {
597 var req = { config: config, section: section };
598
599 if (isa(option, 'Array'))
600 req.options = option;
601 else
602 req.option = option;
603
604 return rcall('uci', 'delete', req, undefined, undefined, cb);
605 },
606
607 delete_all: function(config, type, matches)
608 {
609 return rcall('uci', 'delete', { config: config, type: type, match: matches }, undefined, undefined, cb);
610 },
611
612 foreach: function(config, type, cb)
613 {
614 return rcall('uci', 'get', { config: config, type: type }, 'values', { }, function(sections) {
615 for (var s in sections)
616 cb(sections[s]);
617 });
618 },
619
620 get: function(config, section, option, cb)
621 {
622 return rcall('uci', 'get', { config: config, section: section, option: option }, undefined, { }, function(res) {
623 if (typeof(option) == 'undefined')
624 return retcb(cb, (res.values && res.values['.type']) ? res.values['.type'] : undefined);
625
626 return retcb(cb, res.value);
627 });
628 },
629
630 get_all: function(config, section, cb)
631 {
632 return rcall('uci', 'get', { config: config, section: section }, 'values', { }, cb);
633 },
634
635 get_first: function(config, type, option, cb)
636 {
637 return rcall('uci', 'get', { config: config, type: type }, 'values', { }, function(sections) {
638 for (var s in sections)
639 {
640 var val = (typeof(option) == 'string') ? sections[s][option] : sections[s]['.name'];
641
642 if (typeof(val) != 'undefined')
643 return retcb(cb, val);
644 }
645
646 return retcb(cb, undefined);
647 });
648 },
649
650 section: function(config, type, name, values, cb)
651 {
652 return rcall('uci', 'add', { config: config, type: type, name: name, values: values }, 'section', undefined, cb);
653 },
654
655 set: function(config, section, option, value, cb)
656 {
657 if (typeof(value) == 'undefined' && typeof(option) == 'string')
658 return rcall('uci', 'add', { config: config, section: section, type: option }, undefined, undefined, cb);
659 else if (isa(option, 'Object'))
660 return rcall('uci', 'set', { config: config, section: section, values: option }, undefined, undefined, cb);
661 else
662
663 var values = { };
664 values[option] = value;
665
666 return rcall('uci', 'set', { config: config, section: section, values: values }, undefined, undefined, cb);
667 },
668
669 order: function(config, sections, cb)
670 {
671 return rcall('uci', 'order', { config: config, sections: sections }, undefined, undefined, cb);
672 }
673 };
674
675 this.network = {
676 getNetworkStatus: function(cb)
677 {
678 var ifaces = [ ];
679 var assign = function(target, key)
680 {
681 return function(value) {
682 if (typeof(value) != 'undefined' && !$.isEmptyObject(value))
683 target[key] = value;
684 };
685 };
686
687 return _luci2.rpc.list().then(function(data) {
688 var requests = [ ];
689
690 for (var i = 0; i < data.length; i++)
691 {
692 if (data[i].indexOf('network.interface.') != 0)
693 continue;
694
695 var ifname = data[i].substring(18);
696 if (ifname == 'loopback')
697 continue;
698
699 var iface = { 'name': ifname };
700
701 ifaces.push(iface);
702 requests.push(['network.interface', 'status', { 'interface': ifname }, iface]);
703 }
704
705 return _luci2.rpc.call(requests, function(responses) {
706 for (var key in responses)
707 if (responses[key][0] == 0 && responses[key][1] && responses[key][2])
708 $.extend(responses[key][2], responses[key][1]);
709 });
710 }).then(function() {
711 var requests = [ ];
712
713 for (var i = 0; i < ifaces.length; i++)
714 {
715 var iface = ifaces[i];
716
717 var dev = iface.l3_device || iface.l2_device;
718 if (!dev)
719 continue;
720
721 iface.device = { 'name': dev };
722 requests[dev] = ['network.device', 'status', { 'name': dev }, iface.device];
723 }
724
725 return _luci2.rpc.call(requests, function(responses) {
726 for (var key in responses)
727 if (responses[key][0] == 0 && responses[key][1] && responses[key][2])
728 $.extend(responses[key][2], responses[key][1]);
729 });
730 }).then(function() {
731 var requests = [ ];
732
733 for (var i = 0; i < ifaces.length; i++)
734 {
735 var iface = ifaces[i];
736 if (!iface.device)
737 continue;
738
739 var subdevs = iface.device['bridge-members'];
740 if (!subdevs)
741 continue;
742
743 iface.subdevices = [ ];
744 for (var j = 0; j < subdevs.length; j++)
745 {
746 iface.subdevices[j] = { 'name': subdevs[j] };
747 requests.push(['network.device', 'status', { 'name': subdevs[j] }, iface.subdevices[j]]);
748 }
749 }
750
751 return _luci2.rpc.call(requests, function(responses) {
752 for (var key in responses)
753 if (responses[key][0] == 0 && responses[key][1] && responses[key][2])
754 $.extend(responses[key][2], responses[key][1]);
755 });
756 }).then(function() {
757 var requests = [ ];
758
759 for (var i = 0; i < ifaces.length; i++)
760 {
761 var iface = ifaces[i];
762
763 if (iface.device)
764 requests.push(['iwinfo', 'info', { 'device': iface.device.name }, iface.device]);
765
766 if (iface.subdevices)
767 for (var j = 0; j < iface.subdevices.length; j++)
768 requests.push(['iwinfo', 'info', { 'device': iface.subdevices[j].name }, iface.subdevices[j]]);
769 }
770
771 return _luci2.rpc.call(requests, function(responses) {
772 for (var key in responses)
773 if (responses[key][0] == 0 && responses[key][1] && responses[key][2])
774 if (!$.isEmptyObject(responses[key][1]))
775 responses[key][2].wireless = responses[key][1];
776 });
777 }).then(function() {
778 ifaces.sort(function(a, b) {
779 if (a['interface'] < b['interface'])
780 return -1;
781 else if (a['interface'] > b['interface'])
782 return 1;
783 else
784 return 0;
785 });
786 return retcb(cb, ifaces);
787 });
788 },
789
790 findWanInterfaces: function(cb)
791 {
792 return _luci2.rpc.list().then(function(data) {
793 var requests = { };
794 for (var i = 0; i < data.length; i++)
795 {
796 if (data[i].indexOf('network.interface.') == 0)
797 {
798 var ifname = data[i].substring(18);
799 requests[ifname] = ['network.interface', 'status', { 'interface': ifname }];
800 }
801 }
802 return _luci2.rpc.call(requests);
803 }).then(function(responses) {
804 var rv = [ ];
805 for (var ifname in responses)
806 {
807 var response = responses[ifname];
808
809 if (response[0] != 0 || !response[1] || !response[1].route)
810 continue;
811
812 for (var rn = 0, rt = response[1].route[rn];
813 rn < response[1].route.length;
814 rn++, rt = response[1].route[rn])
815 {
816 if (typeof(rt.table) != 'undefined')
817 continue;
818
819 if (rt.target == '0.0.0.0' && rt.mask == 0)
820 rv[0] = response[1];
821 else if (rt.target == '::' && rt.mask == 0)
822 rv[1] = response[1];
823 }
824 }
825
826 return retcb(cb, rv);
827 });
828 },
829
830 getDHCPLeases: function(cb)
831 {
832 return rcall('luci2.network', 'dhcp_leases', undefined, 'leases', [ ], cb);
833 },
834
835 getDHCPv6Leases: function(cb)
836 {
837 return rcall('luci2.network', 'dhcp6_leases', undefined, 'leases', [ ], cb);
838 },
839
840 getRoutes: function(cb)
841 {
842 return rcall('luci2.network', 'routes', undefined, 'routes', [ ], cb);
843 },
844
845 getIPv6Routes: function(cb)
846 {
847 return rcall('luci2.network', 'routes6', undefined, 'routes', [ ], cb);
848 },
849
850 getARPTable: function(cb)
851 {
852 return rcall('luci2.network', 'arp_table', undefined, 'entries', [ ], cb);
853 },
854
855 getInterfaceStatus: function(iface, cb)
856 {
857 return rcall('network.interface', 'status', { 'interface': iface }, undefined, { }, cb, function(rv) {
858 rv['interface'] = iface;
859 rv['l2_device'] = rv['device'];
860 return rv;
861 });
862 },
863
864 getDeviceStatus: function(dev, cb)
865 {
866 return rcall('network.device', 'status', { name: dev }, undefined, { }, cb, function(rv) {
867 if (typeof(dev) == 'string')
868 rv.device = dev;
869 return rv;
870 });
871 },
872
873 getConntrackCount: function(cb)
874 {
875 return rcall('luci2.network', 'conntrack_count', undefined, undefined, {
876 count: 0,
877 limit: 0
878 }, cb);
879 }
880 };
881
882 this.wireless = {
883 getDevices: function(cb) {
884 return rcall('iwinfo', 'devices', undefined, 'devices', [ ], cb, function(rv) {
885 rv.sort();
886 return rv;
887 });
888 },
889
890 getInfo: function(dev, cb) {
891 var parse_info = function(device, info, rv)
892 {
893 if (!rv[info.phy])
894 rv[info.phy] = {
895 networks: [ ]
896 };
897
898 var phy = rv[info.phy];
899
900 var phy_attrs = [
901 'country', 'channel', 'frequency', 'frequency_offset',
902 'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy'
903 ];
904
905 var net_attrs = [
906 'ssid', 'bssid', 'mode', 'quality', 'quality_max',
907 'signal', 'noise', 'bitrate', 'encryption'
908 ];
909
910 for (var i = 0; i < phy_attrs.length; i++)
911 phy[phy_attrs[i]] = info[phy_attrs[i]];
912
913 var net = {
914 device: device
915 };
916
917 for (var i = 0; i < net_attrs.length; i++)
918 net[net_attrs[i]] = info[net_attrs[i]];
919
920 phy.networks.push(net);
921
922 return phy;
923 };
924
925 if (!dev)
926 {
927 return _luci2.wireless.getDevices().then(function(devices) {
928 var requests = [ ];
929
930 for (var i = 0; i < devices.length; i++)
931 {
932 if (devices[i].indexOf('.sta') >= 0)
933 continue;
934
935 requests[devices[i]] = [ 'iwinfo', 'info', { device: devices[i] } ];
936 }
937
938 return _luci2.rpc.call(requests);
939 }).then(function(responses) {
940 var rv = { };
941
942 for (var device in responses)
943 {
944 var response = responses[device];
945
946 if (response[0] != 0 || !response[1])
947 continue;
948
949 parse_info(device, response[1], rv);
950 }
951
952 return retcb(cb, rv);
953 });
954 }
955
956 return _luci2.rpc.call('iwinfo', 'info', { device: dev }).then(function(response) {
957 if (response[0] != 0 || !response[1])
958 return retcb(cb, { });
959
960 return retcb(cb, parse_info(dev, response[1], { }));
961 });
962 },
963
964 getAssocList: function(dev, cb)
965 {
966 if (!dev)
967 {
968 return _luci2.wireless.getDevices().then(function(devices) {
969 var requests = { };
970
971 for (var i = 0; i < devices.length; i++)
972 {
973 if (devices[i].indexOf('.sta') >= 0)
974 continue;
975
976 requests[devices[i]] = [ 'iwinfo', 'assoclist', { device: devices[i] } ];
977 }
978
979 return _luci2.rpc.call(requests);
980 }).then(function(responses) {
981 var rv = [ ];
982
983 for (var device in responses)
984 {
985 var response = responses[device];
986
987 if (response[0] != 0 || !response[1] || !response[1].results)
988 continue;
989
990 for (var i = 0; i < response[1].results.length; i++)
991 {
992 var station = response[1].results[i];
993
994 station.device = device;
995 rv.push(station);
996 }
997 }
998
999 rv.sort(function(a, b) {
1000 return (a.device == b.device)
1001 ? (a.bssid < b.bssid)
1002 : (a.device > b.device)
1003 ;
1004 });
1005
1006 return retcb(cb, rv);
1007 });
1008 }
1009
1010 return _luci2.rpc.call('iwinfo', 'assoclist', { device: dev }).then(function(response) {
1011 var rv = [ ];
1012
1013 if (response[0] != 0 || !response[1] || !response[1].results)
1014 return retcb(cb, rv);
1015
1016 for (var i = 0; i < response[1].results.length; i++)
1017 {
1018 var station = response[1].results[i];
1019
1020 station.device = dev;
1021 rv.push(station);
1022 }
1023
1024 rv.sort(function(a, b) {
1025 return (a.bssid < b.bssid);
1026 });
1027
1028 return retcb(cb, rv);
1029 });
1030 },
1031
1032 formatEncryption: function(enc)
1033 {
1034 var format_list = function(l, s)
1035 {
1036 var rv = [ ];
1037 for (var i = 0; i < l.length; i++)
1038 rv.push(l[i].toUpperCase());
1039 return rv.join(s ? s : ', ');
1040 }
1041
1042 if (!enc || !enc.enabled)
1043 return _luci2.tr('None');
1044
1045 if (enc.wep)
1046 {
1047 if (enc.wep.length == 2)
1048 return _luci2.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', '));
1049 else if (enc.wep[0] == 'shared')
1050 return _luci2.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', '));
1051 else
1052 return _luci2.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', '));
1053 }
1054 else if (enc.wpa)
1055 {
1056 if (enc.wpa.length == 2)
1057 return _luci2.tr('mixed WPA/WPA2') + ' %s (%s)'.format(
1058 format_list(enc.authentication, '/'),
1059 format_list(enc.ciphers, ', ')
1060 );
1061 else if (enc.wpa[0] == 2)
1062 return 'WPA2 %s (%s)'.format(
1063 format_list(enc.authentication, '/'),
1064 format_list(enc.ciphers, ', ')
1065 );
1066 else
1067 return 'WPA %s (%s)'.format(
1068 format_list(enc.authentication, '/'),
1069 format_list(enc.ciphers, ', ')
1070 );
1071 }
1072
1073 return _luci2.tr('Unknown');
1074 }
1075 };
1076
1077 this.system = {
1078 getInfo: function(cb)
1079 {
1080 return _luci2.rpc.call({
1081 info: [ 'system', 'info', { } ],
1082 board: [ 'system', 'board', { } ],
1083 disk: [ 'luci2.system', 'diskfree', { } ]
1084 }).then(function(responses) {
1085 var rv = { };
1086
1087 if (responses.info[0] == 0)
1088 $.extend(rv, responses.info[1]);
1089
1090 if (responses.board[0] == 0)
1091 $.extend(rv, responses.board[1]);
1092
1093 if (responses.disk[0] == 0)
1094 $.extend(rv, responses.disk[1]);
1095
1096 return retcb(cb, rv);
1097 });
1098 },
1099
1100 getProcessList: function(cb)
1101 {
1102 return rcall('luci2.system', 'process_list', undefined, 'processes', [ ], cb, function(rv) {
1103 rv.sort(function(a, b) { return a.pid - b.pid });
1104 return rv;
1105 });
1106 },
1107
1108 getSystemLog: function(cb)
1109 {
1110 return rcall('luci2.system', 'syslog', undefined, 'log', '', cb);
1111 },
1112
1113 getKernelLog: function(cb)
1114 {
1115 return rcall('luci2.system', 'dmesg', undefined, 'log', '', cb);
1116 },
1117
1118 getZoneInfo: function(cb)
1119 {
1120 return $.getJSON(_luci2.globals.resource + '/zoneinfo.json', cb);
1121 },
1122
1123 canSendSignal: function(cb)
1124 {
1125 return _luci2.rpc.access('ubus', 'luci2.system', 'process_signal', cb);
1126 },
1127
1128 sendSignal: function(pid, sig, cb)
1129 {
1130 return _luci2.rpc.call('luci2.system', 'process_signal', { pid: pid, signal: sig }).then(function(response) {
1131 return retcb(cb, response[0] == 0);
1132 });
1133 },
1134
1135 initList: function(cb)
1136 {
1137 return rcall('luci2.system', 'init_list', undefined, 'initscripts', [ ], cb, function(rv) {
1138 rv.sort(function(a, b) { return (a.start || 0) - (b.start || 0) });
1139 return rv;
1140 });
1141 },
1142
1143 initEnabled: function(init, cb)
1144 {
1145 return this.initList(function(list) {
1146 for (var i = 0; i < list.length; i++)
1147 if (list[i].name == init)
1148 return retcb(cb, !!list[i].enabled);
1149
1150 return retcb(cb, false);
1151 });
1152 },
1153
1154 initRun: function(init, action, cb)
1155 {
1156 return _luci2.rpc.call('luci2.system', 'init_action', { name: init, action: action }).then(function(response) {
1157 return retcb(cb, response[0] == 0);
1158 });
1159 },
1160
1161 canInitRun: function(cb)
1162 {
1163 return _luci2.rpc.access('ubus', 'luci2.system', 'init_action', cb);
1164 },
1165
1166 initStart: function(init, cb) { return _luci2.system.initRun(init, 'start', cb) },
1167 initStop: function(init, cb) { return _luci2.system.initRun(init, 'stop', cb) },
1168 initRestart: function(init, cb) { return _luci2.system.initRun(init, 'restart', cb) },
1169 initReload: function(init, cb) { return _luci2.system.initRun(init, 'reload', cb) },
1170 initEnable: function(init, cb) { return _luci2.system.initRun(init, 'enable', cb) },
1171 initDisable: function(init, cb) { return _luci2.system.initRun(init, 'disable', cb) },
1172
1173
1174 getRcLocal: function(cb)
1175 {
1176 return rcall('luci2.system', 'rclocal_get', undefined, 'data', '', cb);
1177 },
1178
1179 setRcLocal: function(data, cb)
1180 {
1181 return rcall('luci2.system', 'rclocal_set', { data: data }, undefined, undefined, cb);
1182 },
1183
1184 canSetRcLocal: function(cb)
1185 {
1186 return _luci2.rpc.access('ubus', 'luci2.system', 'rclocal_set', cb);
1187 },
1188
1189
1190 getCrontab: function(cb)
1191 {
1192 return rcall('luci2.system', 'crontab_get', undefined, 'data', '', cb);
1193 },
1194
1195 setCrontab: function(data, cb)
1196 {
1197 return rcall('luci2.system', 'crontab_set', { data: data }, undefined, undefined, cb);
1198 },
1199
1200 canSetCrontab: function(cb)
1201 {
1202 return _luci2.rpc.access('ubus', 'luci2.system', 'crontab_set', cb);
1203 },
1204
1205
1206 getSSHKeys: function(cb)
1207 {
1208 return rcall('luci2.system', 'sshkeys_get', undefined, 'keys', [ ], cb);
1209 },
1210
1211 setSSHKeys: function(keys, cb)
1212 {
1213 return rcall('luci2.system', 'sshkeys_set', { keys: keys }, undefined, undefined, cb);
1214 },
1215
1216 canSetSSHKeys: function(cb)
1217 {
1218 return _luci2.rpc.access('ubus', 'luci2.system', 'sshkeys_set', cb);
1219 },
1220
1221
1222 setPassword: function(user, pass, cb)
1223 {
1224 return rcall('luci2.system', 'password_set', { user: user, password: pass }, undefined, undefined, cb);
1225 },
1226
1227 canSetPassword: function(cb)
1228 {
1229 return _luci2.rpc.access('ubus', 'luci2.system', 'password_set', cb);
1230 },
1231
1232
1233 listLEDs: function(cb)
1234 {
1235 return rcall('luci2.system', 'led_list', undefined, 'leds', [ ], cb);
1236 },
1237
1238 listUSBDevices: function(cb)
1239 {
1240 return rcall('luci2.system', 'usb_list', undefined, 'devices', [ ], cb);
1241 },
1242
1243
1244 testUpgrade: function(cb)
1245 {
1246 return rcall('luci2.system', 'upgrade_test', undefined, undefined, { }, cb);
1247 },
1248
1249 startUpgrade: function(keep, cb)
1250 {
1251 return rcall('luci2.system', 'upgrade_start', { keep: !!keep }, undefined, undefined, cb);
1252 },
1253
1254 cleanUpgrade: function(cb)
1255 {
1256 return rcall('luci2.system', 'upgrade_clean', undefined, undefined, undefined, cb);
1257 },
1258
1259 canUpgrade: function(cb)
1260 {
1261 return _luci2.rpc.access('ubus', 'luci2.system', 'upgrade_start', cb);
1262 },
1263
1264
1265 restoreBackup: function(cb)
1266 {
1267 return rcall('luci2.system', 'backup_restore', undefined, undefined, undefined, cb);
1268 },
1269
1270 cleanBackup: function(cb)
1271 {
1272 return rcall('luci2.system', 'backup_clean', undefined, undefined, undefined, cb);
1273 },
1274
1275 canRestoreBackup: function(cb)
1276 {
1277 return _luci2.rpc.access('ubus', 'luci2.system', 'backup_restore', cb);
1278 },
1279
1280
1281 getBackupConfig: function(cb)
1282 {
1283 return rcall('luci2.system', 'backup_config_get', undefined, 'config', '', cb);
1284 },
1285
1286 setBackupConfig: function(data, cb)
1287 {
1288 return rcall('luci2.system', 'backup_config_set', { data: data }, undefined, undefined, cb);
1289 },
1290
1291 canSetBackupConfig: function(cb)
1292 {
1293 return _luci2.rpc.access('ubus', 'luci2.system', 'backup_config_set', cb);
1294 },
1295
1296
1297 listBackup: function(cb)
1298 {
1299 return rcall('luci2.system', 'backup_list', undefined, 'files', [ ], cb);
1300 },
1301
1302
1303 performReboot: function(cb)
1304 {
1305 return rcall('luci2.system', 'reboot', undefined, undefined, undefined, cb);
1306 },
1307
1308 canPerformReboot: function(cb)
1309 {
1310 return _luci2.rpc.access('ubus', 'luci2.system', 'reboot', cb);
1311 }
1312 };
1313
1314 this.opkg = {
1315 updateLists: function(cb)
1316 {
1317 return rcall('luci2.opkg', 'update', undefined, undefined, { }, cb);
1318 },
1319
1320 _fetchPackages: function(action, offset, limit, pattern, cb)
1321 {
1322 var packages = [ ];
1323 var reqlimit = Math.min(limit, 100);
1324
1325 if (reqlimit <= 0)
1326 reqlimit = 100;
1327
1328 return _luci2.rpc.call('luci2.opkg', action, { offset: offset, limit: reqlimit, pattern: pattern }).then(function(response) {
1329 if (response[0] != 0 || !response[1] || !response[1].total)
1330 return retcb(cb, { length: 0, total: 0 });
1331
1332 packages.push.apply(packages, response[1].packages);
1333 packages.total = response[1].total;
1334
1335 if (limit <= 0)
1336 limit = response[1].total;
1337
1338 if (packages.length >= limit)
1339 return retcb(cb, packages);
1340
1341 var requests = [ ];
1342 for (var i = offset + packages.length; i < limit; i += 100)
1343 requests.push(['luci2.opkg', action, { offset: i, limit: (Math.min(i + 100, limit) % 100) || 100, pattern: pattern }]);
1344
1345 return _luci2.rpc.call(requests);
1346 }).then(function(responses) {
1347 for (var key in responses)
1348 {
1349 var response = responses[key];
1350
1351 if (response[0] != 0 || !response[1] || !response[1].packages)
1352 continue;
1353
1354 packages.push.apply(packages, response[1].packages);
1355 packages.total = response[1].total;
1356 }
1357
1358 return retcb(cb, packages);
1359 });
1360 },
1361
1362 listPackages: function(offset, limit, pattern, cb)
1363 {
1364 return _luci2.opkg._fetchPackages('list', offset, limit, pattern, cb);
1365 },
1366
1367 installedPackages: function(offset, limit, pattern, cb)
1368 {
1369 return _luci2.opkg._fetchPackages('list_installed', offset, limit, pattern, cb);
1370 },
1371
1372 findPackages: function(offset, limit, pattern, cb)
1373 {
1374 return _luci2.opkg._fetchPackages('find', offset, limit, pattern, cb);
1375 },
1376
1377 installPackage: function(name, cb)
1378 {
1379 return rcall('luci2.opkg', 'install', { 'package': name }, undefined, { }, cb);
1380 },
1381
1382 removePackage: function(name, cb)
1383 {
1384 return rcall('luci2.opkg', 'remove', { 'package': name }, undefined, { }, cb);
1385 },
1386
1387 getConfig: function(cb)
1388 {
1389 return rcall('luci2.opkg', 'config_get', undefined, 'config', '', cb);
1390 },
1391
1392 setConfig: function(data, cb)
1393 {
1394 return rcall('luci2.opkg', 'config_set', { data: data }, undefined, undefined, cb);
1395 },
1396
1397 canInstallPackage: function(cb)
1398 {
1399 return _luci2.rpc.access('ubus', 'luci2.opkg', 'install', cb);
1400 },
1401
1402 canRemovePackage: function(cb)
1403 {
1404 return _luci2.rpc.access('ubus', 'luci2.opkg', 'remove', cb);
1405 },
1406
1407 canSetConfig: function(cb)
1408 {
1409 return _luci2.rpc.access('ubus', 'luci2.opkg', 'config_set', cb);
1410 }
1411 };
1412
1413 this.ui = {
1414
1415 loading: function(enable)
1416 {
1417 var win = $(window);
1418 var body = $('body');
1419 var div = _luci2._modal || (
1420 _luci2._modal = $('<div />')
1421 .addClass('cbi-modal-loader')
1422 .append($('<div />').text(_luci2.tr('Loading data...')))
1423 .appendTo(body)
1424 );
1425
1426 if (enable)
1427 {
1428 body.css('overflow', 'hidden');
1429 body.css('padding', 0);
1430 body.css('width', win.width());
1431 body.css('height', win.height());
1432 div.css('width', win.width());
1433 div.css('height', win.height());
1434 div.show();
1435 }
1436 else
1437 {
1438 div.hide();
1439 body.css('overflow', '');
1440 body.css('padding', '');
1441 body.css('width', '');
1442 body.css('height', '');
1443 }
1444 },
1445
1446 dialog: function(title, content, options)
1447 {
1448 var win = $(window);
1449 var body = $('body');
1450 var div = _luci2._dialog || (
1451 _luci2._dialog = $('<div />')
1452 .addClass('cbi-modal-dialog')
1453 .append($('<div />')
1454 .append($('<div />')
1455 .addClass('cbi-modal-dialog-header'))
1456 .append($('<div />')
1457 .addClass('cbi-modal-dialog-body'))
1458 .append($('<div />')
1459 .addClass('cbi-modal-dialog-footer')
1460 .append($('<button />')
1461 .addClass('cbi-button')
1462 .text(_luci2.tr('Close'))
1463 .click(function() {
1464 $('body')
1465 .css('overflow', '')
1466 .css('padding', '')
1467 .css('width', '')
1468 .css('height', '');
1469
1470 $(this).parent().parent().parent().hide();
1471 }))))
1472 .appendTo(body)
1473 );
1474
1475 if (typeof(options) != 'object')
1476 options = { };
1477
1478 if (title === false)
1479 {
1480 body
1481 .css('overflow', '')
1482 .css('padding', '')
1483 .css('width', '')
1484 .css('height', '');
1485
1486 _luci2._dialog.hide();
1487
1488 return;
1489 }
1490
1491 var cnt = div.children().children('div.cbi-modal-dialog-body');
1492 var ftr = div.children().children('div.cbi-modal-dialog-footer');
1493
1494 ftr.empty();
1495
1496 if (options.style == 'confirm')
1497 {
1498 ftr.append($('<button />')
1499 .addClass('cbi-button')
1500 .text(_luci2.tr('Ok'))
1501 .click(options.confirm || function() { _luci2.ui.dialog(false) }));
1502
1503 ftr.append($('<button />')
1504 .addClass('cbi-button')
1505 .text(_luci2.tr('Cancel'))
1506 .click(options.cancel || function() { _luci2.ui.dialog(false) }));
1507 }
1508 else if (options.style == 'close')
1509 {
1510 ftr.append($('<button />')
1511 .addClass('cbi-button')
1512 .text(_luci2.tr('Close'))
1513 .click(options.close || function() { _luci2.ui.dialog(false) }));
1514 }
1515 else if (options.style == 'wait')
1516 {
1517 ftr.append($('<button />')
1518 .addClass('cbi-button')
1519 .text(_luci2.tr('Close'))
1520 .attr('disabled', true));
1521 }
1522
1523 div.find('div.cbi-modal-dialog-header').text(title);
1524 div.show();
1525
1526 cnt
1527 .css('max-height', Math.floor(win.height() * 0.70) + 'px')
1528 .empty()
1529 .append(content);
1530
1531 div.children()
1532 .css('margin-top', -Math.floor(div.children().height() / 2) + 'px');
1533
1534 body.css('overflow', 'hidden');
1535 body.css('padding', 0);
1536 body.css('width', win.width());
1537 body.css('height', win.height());
1538 div.css('width', win.width());
1539 div.css('height', win.height());
1540 },
1541
1542 upload: function(title, content, options)
1543 {
1544 var form = _luci2._upload || (
1545 _luci2._upload = $('<form />')
1546 .attr('method', 'post')
1547 .attr('action', '/cgi-bin/luci-upload')
1548 .attr('enctype', 'multipart/form-data')
1549 .attr('target', 'cbi-fileupload-frame')
1550 .append($('<p />'))
1551 .append($('<input />')
1552 .attr('type', 'hidden')
1553 .attr('name', 'sessionid')
1554 .attr('value', _luci2.globals.sid))
1555 .append($('<input />')
1556 .attr('type', 'hidden')
1557 .attr('name', 'filename')
1558 .attr('value', options.filename))
1559 .append($('<input />')
1560 .attr('type', 'file')
1561 .attr('name', 'filedata')
1562 .addClass('cbi-input-file'))
1563 .append($('<div />')
1564 .css('width', '100%')
1565 .addClass('progressbar')
1566 .addClass('intermediate')
1567 .append($('<div />')
1568 .css('width', '100%')))
1569 .append($('<iframe />')
1570 .attr('name', 'cbi-fileupload-frame')
1571 .css('width', '1px')
1572 .css('height', '1px')
1573 .css('visibility', 'hidden'))
1574 );
1575
1576 var finish = _luci2._upload_finish_cb || (
1577 _luci2._upload_finish_cb = function(ev) {
1578 $(this).off('load');
1579
1580 var body = (this.contentDocument || this.contentWindow.document).body;
1581 if (body.firstChild.tagName.toLowerCase() == 'pre')
1582 body = body.firstChild;
1583
1584 var json;
1585 try {
1586 json = $.parseJSON(body.innerHTML);
1587 } catch(e) {
1588 json = {
1589 message: _luci2.tr('Invalid server response received'),
1590 error: [ -1, _luci2.tr('Invalid data') ]
1591 };
1592 };
1593
1594 if (json.error)
1595 {
1596 L.ui.dialog(L.tr('File upload'), [
1597 $('<p />').text(_luci2.tr('The file upload failed with the server response below:')),
1598 $('<pre />').addClass('alert-message').text(json.message || json.error[1]),
1599 $('<p />').text(_luci2.tr('In case of network problems try uploading the file again.'))
1600 ], { style: 'close' });
1601 }
1602 else if (typeof(ev.data.cb) == 'function')
1603 {
1604 ev.data.cb(json);
1605 }
1606 }
1607 );
1608
1609 var confirm = _luci2._upload_confirm_cb || (
1610 _luci2._upload_confirm_cb = function() {
1611 var d = _luci2._upload;
1612 var f = d.find('.cbi-input-file');
1613 var b = d.find('.progressbar');
1614 var p = d.find('p');
1615
1616 if (!f.val())
1617 return;
1618
1619 d.find('iframe').on('load', { cb: options.success }, finish);
1620 d.submit();
1621
1622 f.hide();
1623 b.show();
1624 p.text(_luci2.tr('File upload in progress …'));
1625
1626 _luci2._dialog.find('button').prop('disabled', true);
1627 }
1628 );
1629
1630 _luci2._upload.find('.progressbar').hide();
1631 _luci2._upload.find('.cbi-input-file').val('').show();
1632 _luci2._upload.find('p').text(content || _luci2.tr('Select the file to upload and press "%s" to proceed.').format(_luci2.tr('Ok')));
1633
1634 _luci2.ui.dialog(title || _luci2.tr('File upload'), _luci2._upload, {
1635 style: 'confirm',
1636 confirm: confirm
1637 });
1638 },
1639
1640 reconnect: function()
1641 {
1642 var protocols = (location.protocol == 'https:') ? [ 'http', 'https' ] : [ 'http' ];
1643 var ports = (location.protocol == 'https:') ? [ 80, location.port || 443 ] : [ location.port || 80 ];
1644 var address = location.hostname.match(/^[A-Fa-f0-9]*:[A-Fa-f0-9:]+$/) ? '[' + location.hostname + ']' : location.hostname;
1645 var images = $();
1646 var interval, timeout;
1647
1648 _luci2.ui.dialog(
1649 _luci2.tr('Waiting for device'), [
1650 $('<p />').text(_luci2.tr('Please stand by while the device is reconfiguring …')),
1651 $('<div />')
1652 .css('width', '100%')
1653 .addClass('progressbar')
1654 .addClass('intermediate')
1655 .append($('<div />')
1656 .css('width', '100%'))
1657 ], { style: 'wait' }
1658 );
1659
1660 for (var i = 0; i < protocols.length; i++)
1661 images = images.add($('<img />').attr('url', protocols[i] + '://' + address + ':' + ports[i]));
1662
1663 //_luci2.network.getNetworkStatus(function(s) {
1664 // for (var i = 0; i < protocols.length; i++)
1665 // {
1666 // for (var j = 0; j < s.length; j++)
1667 // {
1668 // for (var k = 0; k < s[j]['ipv4-address'].length; k++)
1669 // images = images.add($('<img />').attr('url', protocols[i] + '://' + s[j]['ipv4-address'][k].address + ':' + ports[i]));
1670 //
1671 // for (var l = 0; l < s[j]['ipv6-address'].length; l++)
1672 // images = images.add($('<img />').attr('url', protocols[i] + '://[' + s[j]['ipv6-address'][l].address + ']:' + ports[i]));
1673 // }
1674 // }
1675 //}).then(function() {
1676 images.on('load', function() {
1677 var url = this.getAttribute('url');
1678 _luci2.rpc.access('ubus', 'session', 'access').then(function(response) {
1679 if (response[0] == 0)
1680 {
1681 window.clearTimeout(timeout);
1682 window.clearInterval(interval);
1683 _luci2.ui.dialog(false);
1684 images = null;
1685 }
1686 else
1687 {
1688 location.href = url;
1689 }
1690 });
1691 });
1692
1693 interval = window.setInterval(function() {
1694 images.each(function() {
1695 this.setAttribute('src', this.getAttribute('url') + _luci2.globals.resource + '/icons/loading.gif?r=' + Math.random());
1696 });
1697 }, 5000);
1698
1699 timeout = window.setTimeout(function() {
1700 window.clearInterval(interval);
1701 images.off('load');
1702
1703 _luci2.ui.dialog(
1704 _luci2.tr('Device not responding'),
1705 _luci2.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'),
1706 { style: 'close' }
1707 );
1708 }, 180000);
1709 //});
1710 },
1711
1712 login: function(invalid)
1713 {
1714 if (!_luci2._login_deferred || _luci2._login_deferred.state() != 'pending')
1715 _luci2._login_deferred = $.Deferred();
1716
1717 /* try to find sid from hash */
1718 var sid = _luci2.getHash('id');
1719 if (sid && sid.match(/^[a-f0-9]{32}$/))
1720 {
1721 _luci2.globals.sid = sid;
1722 _luci2.rpc.access('ubus', 'session', 'access').then(function(response) {
1723 if (response[0] == 0)
1724 {
1725 _luci2._login_deferred.resolve();
1726 }
1727 else
1728 {
1729 _luci2.setHash('id', undefined);
1730 _luci2.ui.login();
1731 }
1732 });
1733
1734 return _luci2._login_deferred;
1735 }
1736
1737 var form = _luci2._login || (
1738 _luci2._login = $('<div />')
1739 .append($('<p />')
1740 .addClass('alert-message')
1741 .text(_luci2.tr('Wrong username or password given!')))
1742 .append($('<p />')
1743 .append($('<label />')
1744 .text(_luci2.tr('Username'))
1745 .append($('<br />'))
1746 .append($('<input />')
1747 .attr('type', 'text')
1748 .attr('name', 'username')
1749 .attr('value', 'root')
1750 .addClass('cbi-input-text'))))
1751 .append($('<p />')
1752 .append($('<label />')
1753 .text(_luci2.tr('Password'))
1754 .append($('<br />'))
1755 .append($('<input />')
1756 .attr('type', 'password')
1757 .attr('name', 'password')
1758 .addClass('cbi-input-password'))))
1759 .append($('<p />')
1760 .text(_luci2.tr('Enter your username and password above, then click "%s" to proceed.').format(_luci2.tr('Ok'))))
1761 );
1762
1763 var response = _luci2._login_response_cb || (
1764 _luci2._login_response_cb = function(data) {
1765 if (typeof(data) == 'object' && typeof(data.sessionid) == 'string')
1766 {
1767 _luci2.globals.sid = data.sessionid;
1768 _luci2.setHash('id', _luci2.globals.sid);
1769
1770 _luci2.ui.dialog(false);
1771 _luci2._login_deferred.resolve();
1772 }
1773 else
1774 {
1775 _luci2.ui.login(true);
1776 }
1777 }
1778 );
1779
1780 var confirm = _luci2._login_confirm_cb || (
1781 _luci2._login_confirm_cb = function() {
1782 var d = _luci2._login;
1783 var u = d.find('[name=username]').val();
1784 var p = d.find('[name=password]').val();
1785
1786 if (!u)
1787 return;
1788
1789 _luci2.ui.dialog(
1790 _luci2.tr('Logging in'), [
1791 $('<p />').text(_luci2.tr('Log in in progress …')),
1792 $('<div />')
1793 .css('width', '100%')
1794 .addClass('progressbar')
1795 .addClass('intermediate')
1796 .append($('<div />')
1797 .css('width', '100%'))
1798 ], { style: 'wait' }
1799 );
1800
1801 $.ajax('/cgi-bin/luci-login', {
1802 type: 'POST',
1803 cache: false,
1804 data: { username: u, password: p },
1805 dataType: 'json',
1806 success: response,
1807 error: response
1808 });
1809 }
1810 );
1811
1812 if (invalid)
1813 form.find('.alert-message').show();
1814 else
1815 form.find('.alert-message').hide();
1816
1817 _luci2.ui.dialog(_luci2.tr('Authorization Required'), form, {
1818 style: 'confirm',
1819 confirm: confirm
1820 });
1821
1822 return _luci2._login_deferred;
1823 },
1824
1825 renderMainMenu: function()
1826 {
1827 return rcall('luci2.ui', 'menu', undefined, 'menu', { }, function(entries) {
1828 _luci2.globals.mainMenu = new _luci2.ui.menu();
1829 _luci2.globals.mainMenu.entries(entries);
1830
1831 $('#mainmenu')
1832 .empty()
1833 .append(_luci2.globals.mainMenu.render(0, 1));
1834 });
1835 },
1836
1837 renderViewMenu: function()
1838 {
1839 $('#viewmenu')
1840 .empty()
1841 .append(_luci2.globals.mainMenu.render(2, 900));
1842 },
1843
1844 renderView: function(node)
1845 {
1846 var name = node.view.split(/\//).join('.');
1847
1848 _luci2.ui.renderViewMenu();
1849
1850 if (!_luci2._views)
1851 _luci2._views = { };
1852
1853 _luci2.setHash('view', node.view);
1854
1855 if (_luci2._views[name] instanceof _luci2.ui.view)
1856 return _luci2._views[name].render();
1857
1858 return $.ajax(_luci2.globals.resource + '/view/' + name + '.js', {
1859 method: 'GET',
1860 cache: true,
1861 dataType: 'text'
1862 }).then(function(data) {
1863 try {
1864 var viewConstructor = (new Function(['L', '$'], 'return ' + data))(_luci2, $);
1865
1866 _luci2._views[name] = new viewConstructor({
1867 name: name,
1868 acls: node.write || { }
1869 });
1870
1871 return _luci2._views[name].render();
1872 }
1873 catch(e) { };
1874
1875 return $.Deferred().resolve();
1876 });
1877 },
1878
1879 init: function()
1880 {
1881 _luci2.ui.loading(true);
1882
1883 $.when(
1884 _luci2.ui.renderMainMenu()
1885 ).then(function() {
1886 _luci2.ui.renderView(_luci2.globals.defaultNode).then(function() {
1887 _luci2.ui.loading(false);
1888 })
1889 });
1890 }
1891 };
1892
1893 var AbstractWidget = Class.extend({
1894 i18n: function(text) {
1895 return text;
1896 },
1897
1898 toString: function() {
1899 var x = document.createElement('div');
1900 x.appendChild(this.render());
1901
1902 return x.innerHTML;
1903 },
1904
1905 insertInto: function(id) {
1906 return $(id).empty().append(this.render());
1907 }
1908 });
1909
1910 this.ui.view = AbstractWidget.extend({
1911 _fetch_template: function()
1912 {
1913 return $.ajax(_luci2.globals.resource + '/template/' + this.options.name + '.htm', {
1914 method: 'GET',
1915 cache: true,
1916 dataType: 'text',
1917 success: function(data) {
1918 data = data.replace(/<%([#:=])?(.+?)%>/g, function(match, p1, p2) {
1919 p2 = p2.replace(/^\s+/, '').replace(/\s+$/, '');
1920 switch (p1)
1921 {
1922 case '#':
1923 return '';
1924
1925 case ':':
1926 return _luci2.tr(p2);
1927
1928 case '=':
1929 return _luci2.globals[p2] || '';
1930
1931 default:
1932 return '(?' + match + ')';
1933 }
1934 });
1935
1936 $('#maincontent').append(data);
1937 }
1938 });
1939 },
1940
1941 execute: function()
1942 {
1943 throw "Not implemented";
1944 },
1945
1946 render: function()
1947 {
1948 var container = $('#maincontent');
1949
1950 container.empty();
1951
1952 if (this.title)
1953 container.append($('<h2 />').append(this.title));
1954
1955 if (this.description)
1956 container.append($('<div />').addClass('cbi-map-descr').append(this.description));
1957
1958 var self = this;
1959 return this._fetch_template().then(function() {
1960 return _luci2.deferrable(self.execute());
1961 });
1962 }
1963 });
1964
1965 this.ui.menu = AbstractWidget.extend({
1966 init: function() {
1967 this._nodes = { };
1968 },
1969
1970 entries: function(entries)
1971 {
1972 for (var entry in entries)
1973 {
1974 var path = entry.split(/\//);
1975 var node = this._nodes;
1976
1977 for (i = 0; i < path.length; i++)
1978 {
1979 if (!node.childs)
1980 node.childs = { };
1981
1982 if (!node.childs[path[i]])
1983 node.childs[path[i]] = { };
1984
1985 node = node.childs[path[i]];
1986 }
1987
1988 $.extend(node, entries[entry]);
1989 }
1990 },
1991
1992 _indexcmp: function(a, b)
1993 {
1994 var x = a.index || 0;
1995 var y = b.index || 0;
1996 return (x - y);
1997 },
1998
1999 firstChildView: function(node)
2000 {
2001 if (node.view)
2002 return node;
2003
2004 var nodes = [ ];
2005 for (var child in (node.childs || { }))
2006 nodes.push(node.childs[child]);
2007
2008 nodes.sort(this._indexcmp);
2009
2010 for (var i = 0; i < nodes.length; i++)
2011 {
2012 var child = this.firstChildView(nodes[i]);
2013 if (child)
2014 {
2015 $.extend(node, child);
2016 return node;
2017 }
2018 }
2019
2020 return undefined;
2021 },
2022
2023 _onclick: function(ev)
2024 {
2025 _luci2.ui.loading(true);
2026 _luci2.ui.renderView(ev.data).then(function() {
2027 _luci2.ui.loading(false);
2028 });
2029
2030 ev.preventDefault();
2031 this.blur();
2032 },
2033
2034 _render: function(childs, level, min, max)
2035 {
2036 var nodes = [ ];
2037 for (var node in childs)
2038 {
2039 var child = this.firstChildView(childs[node]);
2040 if (child)
2041 nodes.push(childs[node]);
2042 }
2043
2044 nodes.sort(this._indexcmp);
2045
2046 var list = $('<ul />');
2047
2048 if (level == 0)
2049 list.addClass('nav');
2050 else if (level == 1)
2051 list.addClass('dropdown-menu');
2052
2053 for (var i = 0; i < nodes.length; i++)
2054 {
2055 if (!_luci2.globals.defaultNode)
2056 {
2057 var v = _luci2.getHash('view');
2058 if (!v || v == nodes[i].view)
2059 _luci2.globals.defaultNode = nodes[i];
2060 }
2061
2062 var item = $('<li />')
2063 .append($('<a />')
2064 .attr('href', '#')
2065 .text(_luci2.tr(nodes[i].title))
2066 .click(nodes[i], this._onclick))
2067 .appendTo(list);
2068
2069 if (nodes[i].childs && level < max)
2070 {
2071 item.addClass('dropdown');
2072 item.find('a').addClass('menu');
2073 item.append(this._render(nodes[i].childs, level + 1));
2074 }
2075 }
2076
2077 return list.get(0);
2078 },
2079
2080 render: function(min, max)
2081 {
2082 var top = min ? this.getNode(_luci2.globals.defaultNode.view, min) : this._nodes;
2083 return this._render(top.childs, 0, min, max);
2084 },
2085
2086 getNode: function(path, max)
2087 {
2088 var p = path.split(/\//);
2089 var n = this._nodes;
2090
2091 if (typeof(max) == 'undefined')
2092 max = p.length;
2093
2094 for (var i = 0; i < max; i++)
2095 {
2096 if (!n.childs[p[i]])
2097 return undefined;
2098
2099 n = n.childs[p[i]];
2100 }
2101
2102 return n;
2103 }
2104 });
2105
2106 this.ui.table = AbstractWidget.extend({
2107 init: function()
2108 {
2109 this._rows = [ ];
2110 },
2111
2112 row: function(values)
2113 {
2114 if (isa(values, 'Array'))
2115 {
2116 this._rows.push(values);
2117 }
2118 else if (isa(values, 'Object'))
2119 {
2120 var v = [ ];
2121 for (var i = 0; i < this.options.columns.length; i++)
2122 {
2123 var col = this.options.columns[i];
2124
2125 if (typeof col.key == 'string')
2126 v.push(values[col.key]);
2127 else
2128 v.push(null);
2129 }
2130 this._rows.push(v);
2131 }
2132 },
2133
2134 rows: function(rows)
2135 {
2136 for (var i = 0; i < rows.length; i++)
2137 this.row(rows[i]);
2138 },
2139
2140 render: function(id)
2141 {
2142 var fieldset = document.createElement('fieldset');
2143 fieldset.className = 'cbi-section';
2144
2145 if (this.options.caption)
2146 {
2147 var legend = document.createElement('legend');
2148 $(legend).append(this.options.caption);
2149 fieldset.appendChild(legend);
2150 }
2151
2152 var table = document.createElement('table');
2153 table.className = 'cbi-section-table';
2154
2155 var has_caption = false;
2156 var has_description = false;
2157
2158 for (var i = 0; i < this.options.columns.length; i++)
2159 if (this.options.columns[i].caption)
2160 {
2161 has_caption = true;
2162 break;
2163 }
2164 else if (this.options.columns[i].description)
2165 {
2166 has_description = true;
2167 break;
2168 }
2169
2170 if (has_caption)
2171 {
2172 var tr = table.insertRow(-1);
2173 tr.className = 'cbi-section-table-titles';
2174
2175 for (var i = 0; i < this.options.columns.length; i++)
2176 {
2177 var col = this.options.columns[i];
2178 var th = document.createElement('th');
2179 th.className = 'cbi-section-table-cell';
2180
2181 tr.appendChild(th);
2182
2183 if (col.width)
2184 th.style.width = col.width;
2185
2186 if (col.align)
2187 th.style.textAlign = col.align;
2188
2189 if (col.caption)
2190 $(th).append(col.caption);
2191 }
2192 }
2193
2194 if (has_description)
2195 {
2196 var tr = table.insertRow(-1);
2197 tr.className = 'cbi-section-table-descr';
2198
2199 for (var i = 0; i < this.options.columns.length; i++)
2200 {
2201 var col = this.options.columns[i];
2202 var th = document.createElement('th');
2203 th.className = 'cbi-section-table-cell';
2204
2205 tr.appendChild(th);
2206
2207 if (col.width)
2208 th.style.width = col.width;
2209
2210 if (col.align)
2211 th.style.textAlign = col.align;
2212
2213 if (col.description)
2214 $(th).append(col.description);
2215 }
2216 }
2217
2218 if (this._rows.length == 0)
2219 {
2220 if (this.options.placeholder)
2221 {
2222 var tr = table.insertRow(-1);
2223 var td = tr.insertCell(-1);
2224 td.className = 'cbi-section-table-cell';
2225
2226 td.colSpan = this.options.columns.length;
2227 $(td).append(this.options.placeholder);
2228 }
2229 }
2230 else
2231 {
2232 for (var i = 0; i < this._rows.length; i++)
2233 {
2234 var tr = table.insertRow(-1);
2235
2236 for (var j = 0; j < this.options.columns.length; j++)
2237 {
2238 var col = this.options.columns[j];
2239 var td = tr.insertCell(-1);
2240
2241 var val = this._rows[i][j];
2242
2243 if (typeof(val) == 'undefined')
2244 val = col.placeholder;
2245
2246 if (typeof(val) == 'undefined')
2247 val = '';
2248
2249 if (col.width)
2250 td.style.width = col.width;
2251
2252 if (col.align)
2253 td.style.textAlign = col.align;
2254
2255 if (typeof col.format == 'string')
2256 $(td).append(col.format.format(val));
2257 else if (typeof col.format == 'function')
2258 $(td).append(col.format(val, i));
2259 else
2260 $(td).append(val);
2261 }
2262 }
2263 }
2264
2265 this._rows = [ ];
2266 fieldset.appendChild(table);
2267
2268 return fieldset;
2269 }
2270 });
2271
2272 this.ui.progress = AbstractWidget.extend({
2273 render: function()
2274 {
2275 var vn = parseInt(this.options.value) || 0;
2276 var mn = parseInt(this.options.max) || 100;
2277 var pc = Math.floor((100 / mn) * vn);
2278
2279 var bar = document.createElement('div');
2280 bar.className = 'progressbar';
2281
2282 bar.appendChild(document.createElement('div'));
2283 bar.lastChild.appendChild(document.createElement('div'));
2284 bar.lastChild.style.width = pc + '%';
2285
2286 if (typeof(this.options.format) == 'string')
2287 $(bar.lastChild.lastChild).append(this.options.format.format(this.options.value, this.options.max, pc));
2288 else if (typeof(this.options.format) == 'function')
2289 $(bar.lastChild.lastChild).append(this.options.format(pc));
2290 else
2291 $(bar.lastChild.lastChild).append('%.2f%%'.format(pc));
2292
2293 return bar;
2294 }
2295 });
2296
2297 this.ui.devicebadge = AbstractWidget.extend({
2298 render: function()
2299 {
2300 var dev = this.options.l3_device || this.options.device || '?';
2301
2302 var span = document.createElement('span');
2303 span.className = 'ifacebadge';
2304
2305 if (typeof(this.options.signal) == 'number' ||
2306 typeof(this.options.noise) == 'number')
2307 {
2308 var r = 'none';
2309 if (typeof(this.options.signal) != 'undefined' &&
2310 typeof(this.options.noise) != 'undefined')
2311 {
2312 var q = (-1 * (this.options.noise - this.options.signal)) / 5;
2313 if (q < 1)
2314 r = '0';
2315 else if (q < 2)
2316 r = '0-25';
2317 else if (q < 3)
2318 r = '25-50';
2319 else if (q < 4)
2320 r = '50-75';
2321 else
2322 r = '75-100';
2323 }
2324
2325 span.appendChild(document.createElement('img'));
2326 span.lastChild.src = _luci2.globals.resource + '/icons/signal-' + r + '.png';
2327
2328 if (r == 'none')
2329 span.title = _luci2.tr('No signal');
2330 else
2331 span.title = '%s: %d %s / %s: %d %s'.format(
2332 _luci2.tr('Signal'), this.options.signal, _luci2.tr('dBm'),
2333 _luci2.tr('Noise'), this.options.noise, _luci2.tr('dBm')
2334 );
2335 }
2336 else
2337 {
2338 var type = 'ethernet';
2339 var desc = _luci2.tr('Ethernet device');
2340
2341 if (this.options.l3_device != this.options.device)
2342 {
2343 type = 'tunnel';
2344 desc = _luci2.tr('Tunnel interface');
2345 }
2346 else if (dev.indexOf('br-') == 0)
2347 {
2348 type = 'bridge';
2349 desc = _luci2.tr('Bridge');
2350 }
2351 else if (dev.indexOf('.') > 0)
2352 {
2353 type = 'vlan';
2354 desc = _luci2.tr('VLAN interface');
2355 }
2356 else if (dev.indexOf('wlan') == 0 ||
2357 dev.indexOf('ath') == 0 ||
2358 dev.indexOf('wl') == 0)
2359 {
2360 type = 'wifi';
2361 desc = _luci2.tr('Wireless Network');
2362 }
2363
2364 span.appendChild(document.createElement('img'));
2365 span.lastChild.src = _luci2.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
2366 span.title = desc;
2367 }
2368
2369 $(span).append(' ');
2370 $(span).append(dev);
2371
2372 return span;
2373 }
2374 });
2375
2376 var type = function(f, l)
2377 {
2378 f.message = l;
2379 return f;
2380 };
2381
2382 this.cbi = {
2383 validation: {
2384 i18n: function(msg)
2385 {
2386 _luci2.cbi.validation.message = _luci2.tr(msg);
2387 },
2388
2389 compile: function(code)
2390 {
2391 var pos = 0;
2392 var esc = false;
2393 var depth = 0;
2394 var types = _luci2.cbi.validation.types;
2395 var stack = [ ];
2396
2397 code += ',';
2398
2399 for (var i = 0; i < code.length; i++)
2400 {
2401 if (esc)
2402 {
2403 esc = false;
2404 continue;
2405 }
2406
2407 switch (code.charCodeAt(i))
2408 {
2409 case 92:
2410 esc = true;
2411 break;
2412
2413 case 40:
2414 case 44:
2415 if (depth <= 0)
2416 {
2417 if (pos < i)
2418 {
2419 var label = code.substring(pos, i);
2420 label = label.replace(/\\(.)/g, '$1');
2421 label = label.replace(/^[ \t]+/g, '');
2422 label = label.replace(/[ \t]+$/g, '');
2423
2424 if (label && !isNaN(label))
2425 {
2426 stack.push(parseFloat(label));
2427 }
2428 else if (label.match(/^(['"]).*\1$/))
2429 {
2430 stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
2431 }
2432 else if (typeof types[label] == 'function')
2433 {
2434 stack.push(types[label]);
2435 stack.push(null);
2436 }
2437 else
2438 {
2439 throw "Syntax error, unhandled token '"+label+"'";
2440 }
2441 }
2442 pos = i+1;
2443 }
2444 depth += (code.charCodeAt(i) == 40);
2445 break;
2446
2447 case 41:
2448 if (--depth <= 0)
2449 {
2450 if (typeof stack[stack.length-2] != 'function')
2451 throw "Syntax error, argument list follows non-function";
2452
2453 stack[stack.length-1] =
2454 arguments.callee(code.substring(pos, i));
2455
2456 pos = i+1;
2457 }
2458 break;
2459 }
2460 }
2461
2462 return stack;
2463 }
2464 }
2465 };
2466
2467 var validation = this.cbi.validation;
2468
2469 validation.types = {
2470 'integer': function()
2471 {
2472 if (this.match(/^-?[0-9]+$/) != null)
2473 return true;
2474
2475 validation.i18n('Must be a valid integer');
2476 return false;
2477 },
2478
2479 'uinteger': function()
2480 {
2481 if (validation.types['integer'].apply(this) && (this >= 0))
2482 return true;
2483
2484 validation.i18n('Must be a positive integer');
2485 return false;
2486 },
2487
2488 'float': function()
2489 {
2490 if (!isNaN(parseFloat(this)))
2491 return true;
2492
2493 validation.i18n('Must be a valid number');
2494 return false;
2495 },
2496
2497 'ufloat': function()
2498 {
2499 if (validation.types['float'].apply(this) && (this >= 0))
2500 return true;
2501
2502 validation.i18n('Must be a positive number');
2503 return false;
2504 },
2505
2506 'ipaddr': function()
2507 {
2508 if (validation.types['ip4addr'].apply(this) ||
2509 validation.types['ip6addr'].apply(this))
2510 return true;
2511
2512 validation.i18n('Must be a valid IP address');
2513 return false;
2514 },
2515
2516 'ip4addr': function()
2517 {
2518 if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/))
2519 {
2520 if ((RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
2521 (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
2522 (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
2523 (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
2524 ((RegExp.$6.indexOf('.') < 0)
2525 ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32))
2526 : (validation.types['ip4addr'].apply(RegExp.$6))))
2527 return true;
2528 }
2529
2530 validation.i18n('Must be a valid IPv4 address');
2531 return false;
2532 },
2533
2534 'ip6addr': function()
2535 {
2536 if (this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/))
2537 {
2538 if (!RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)))
2539 {
2540 var addr = RegExp.$1;
2541
2542 if (addr == '::')
2543 {
2544 return true;
2545 }
2546
2547 if (addr.indexOf('.') > 0)
2548 {
2549 var off = addr.lastIndexOf(':');
2550
2551 if (!(off && validation.types['ip4addr'].apply(addr.substr(off+1))))
2552 {
2553 validation.i18n('Must be a valid IPv6 address');
2554 return false;
2555 }
2556
2557 addr = addr.substr(0, off) + ':0:0';
2558 }
2559
2560 if (addr.indexOf('::') >= 0)
2561 {
2562 var colons = 0;
2563 var fill = '0';
2564
2565 for (var i = 1; i < (addr.length-1); i++)
2566 if (addr.charAt(i) == ':')
2567 colons++;
2568
2569 if (colons > 7)
2570 {
2571 validation.i18n('Must be a valid IPv6 address');
2572 return false;
2573 }
2574
2575 for (var i = 0; i < (7 - colons); i++)
2576 fill += ':0';
2577
2578 if (addr.match(/^(.*?)::(.*?)$/))
2579 addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
2580 (RegExp.$2 ? ':' + RegExp.$2 : '');
2581 }
2582
2583 if (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null)
2584 return true;
2585
2586 validation.i18n('Must be a valid IPv6 address');
2587 return false;
2588 }
2589 }
2590
2591 return false;
2592 },
2593
2594 'port': function()
2595 {
2596 if (validation.types['integer'].apply(this) &&
2597 (this >= 0) && (this <= 65535))
2598 return true;
2599
2600 validation.i18n('Must be a valid port number');
2601 return false;
2602 },
2603
2604 'portrange': function()
2605 {
2606 if (this.match(/^(\d+)-(\d+)$/))
2607 {
2608 var p1 = RegExp.$1;
2609 var p2 = RegExp.$2;
2610
2611 if (validation.types['port'].apply(p1) &&
2612 validation.types['port'].apply(p2) &&
2613 (parseInt(p1) <= parseInt(p2)))
2614 return true;
2615 }
2616 else if (validation.types['port'].apply(this))
2617 {
2618 return true;
2619 }
2620
2621 validation.i18n('Must be a valid port range');
2622 return false;
2623 },
2624
2625 'macaddr': function()
2626 {
2627 if (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null)
2628 return true;
2629
2630 validation.i18n('Must be a valid MAC address');
2631 return false;
2632 },
2633
2634 'host': function()
2635 {
2636 if (validation.types['hostname'].apply(this) ||
2637 validation.types['ipaddr'].apply(this))
2638 return true;
2639
2640 validation.i18n('Must be a valid hostname or IP address');
2641 return false;
2642 },
2643
2644 'hostname': function()
2645 {
2646 if ((this.length <= 253) &&
2647 ((this.match(/^[a-zA-Z0-9]+$/) != null ||
2648 (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
2649 this.match(/[^0-9.]/)))))
2650 return true;
2651
2652 validation.i18n('Must be a valid host name');
2653 return false;
2654 },
2655
2656 'network': function()
2657 {
2658 if (validation.types['uciname'].apply(this) ||
2659 validation.types['host'].apply(this))
2660 return true;
2661
2662 validation.i18n('Must be a valid network name');
2663 return false;
2664 },
2665
2666 'wpakey': function()
2667 {
2668 var v = this;
2669
2670 if ((v.length == 64)
2671 ? (v.match(/^[a-fA-F0-9]{64}$/) != null)
2672 : ((v.length >= 8) && (v.length <= 63)))
2673 return true;
2674
2675 validation.i18n('Must be a valid WPA key');
2676 return false;
2677 },
2678
2679 'wepkey': function()
2680 {
2681 var v = this;
2682
2683 if (v.substr(0,2) == 's:')
2684 v = v.substr(2);
2685
2686 if (((v.length == 10) || (v.length == 26))
2687 ? (v.match(/^[a-fA-F0-9]{10,26}$/) != null)
2688 : ((v.length == 5) || (v.length == 13)))
2689 return true;
2690
2691 validation.i18n('Must be a valid WEP key');
2692 return false;
2693 },
2694
2695 'uciname': function()
2696 {
2697 if (this.match(/^[a-zA-Z0-9_]+$/) != null)
2698 return true;
2699
2700 validation.i18n('Must be a valid UCI identifier');
2701 return false;
2702 },
2703
2704 'range': function(min, max)
2705 {
2706 var val = parseFloat(this);
2707
2708 if (validation.types['integer'].apply(this) &&
2709 !isNaN(min) && !isNaN(max) && ((val >= min) && (val <= max)))
2710 return true;
2711
2712 validation.i18n('Must be a number between %d and %d');
2713 return false;
2714 },
2715
2716 'min': function(min)
2717 {
2718 var val = parseFloat(this);
2719
2720 if (validation.types['integer'].apply(this) &&
2721 !isNaN(min) && !isNaN(val) && (val >= min))
2722 return true;
2723
2724 validation.i18n('Must be a number greater or equal to %d');
2725 return false;
2726 },
2727
2728 'max': function(max)
2729 {
2730 var val = parseFloat(this);
2731
2732 if (validation.types['integer'].apply(this) &&
2733 !isNaN(max) && !isNaN(val) && (val <= max))
2734 return true;
2735
2736 validation.i18n('Must be a number lower or equal to %d');
2737 return false;
2738 },
2739
2740 'rangelength': function(min, max)
2741 {
2742 var val = '' + this;
2743
2744 if (!isNaN(min) && !isNaN(max) &&
2745 (val.length >= min) && (val.length <= max))
2746 return true;
2747
2748 validation.i18n('Must be between %d and %d characters');
2749 return false;
2750 },
2751
2752 'minlength': function(min)
2753 {
2754 var val = '' + this;
2755
2756 if (!isNaN(min) && (val.length >= min))
2757 return true;
2758
2759 validation.i18n('Must be at least %d characters');
2760 return false;
2761 },
2762
2763 'maxlength': function(max)
2764 {
2765 var val = '' + this;
2766
2767 if (!isNaN(max) && (val.length <= max))
2768 return true;
2769
2770 validation.i18n('Must be at most %d characters');
2771 return false;
2772 },
2773
2774 'or': function()
2775 {
2776 var msgs = [ ];
2777
2778 for (var i = 0; i < arguments.length; i += 2)
2779 {
2780 delete validation.message;
2781
2782 if (typeof(arguments[i]) != 'function')
2783 {
2784 if (arguments[i] == this)
2785 return true;
2786 i--;
2787 }
2788 else if (arguments[i].apply(this, arguments[i+1]))
2789 {
2790 return true;
2791 }
2792
2793 if (validation.message)
2794 msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
2795 }
2796
2797 validation.message = msgs.join( _luci2.tr(' - or - '));
2798 return false;
2799 },
2800
2801 'and': function()
2802 {
2803 var msgs = [ ];
2804
2805 for (var i = 0; i < arguments.length; i += 2)
2806 {
2807 delete validation.message;
2808
2809 if (typeof arguments[i] != 'function')
2810 {
2811 if (arguments[i] != this)
2812 return false;
2813 i--;
2814 }
2815 else if (!arguments[i].apply(this, arguments[i+1]))
2816 {
2817 return false;
2818 }
2819
2820 if (validation.message)
2821 msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
2822 }
2823
2824 validation.message = msgs.join(', ');
2825 return true;
2826 },
2827
2828 'neg': function()
2829 {
2830 return validation.types['or'].apply(
2831 this.replace(/^[ \t]*![ \t]*/, ''), arguments);
2832 },
2833
2834 'list': function(subvalidator, subargs)
2835 {
2836 if (typeof subvalidator != 'function')
2837 return false;
2838
2839 var tokens = this.match(/[^ \t]+/g);
2840 for (var i = 0; i < tokens.length; i++)
2841 if (!subvalidator.apply(tokens[i], subargs))
2842 return false;
2843
2844 return true;
2845 },
2846
2847 'phonedigit': function()
2848 {
2849 if (this.match(/^[0-9\*#!\.]+$/) != null)
2850 return true;
2851
2852 validation.i18n('Must be a valid phone number digit');
2853 return false;
2854 },
2855
2856 'string': function()
2857 {
2858 return true;
2859 }
2860 };
2861
2862
2863 var AbstractValue = AbstractWidget.extend({
2864 init: function(name, options)
2865 {
2866 this.name = name;
2867 this.instance = { };
2868 this.dependencies = [ ];
2869 this.rdependency = { };
2870
2871 this.options = _luci2.defaults(options, {
2872 placeholder: '',
2873 datatype: 'string',
2874 optional: false,
2875 keep: true
2876 });
2877 },
2878
2879 id: function(sid)
2880 {
2881 return this.section.id('field', sid || '__unknown__', this.name);
2882 },
2883
2884 render: function(sid)
2885 {
2886 var i = this.instance[sid] = { };
2887
2888 i.top = $('<div />').addClass('cbi-value');
2889
2890 if (typeof(this.options.caption) == 'string')
2891 $('<label />')
2892 .addClass('cbi-value-title')
2893 .attr('for', this.id(sid))
2894 .text(this.options.caption)
2895 .appendTo(i.top);
2896
2897 i.widget = $('<div />').addClass('cbi-value-field').append(this.widget(sid)).appendTo(i.top);
2898 i.error = $('<div />').addClass('cbi-value-error').appendTo(i.top);
2899
2900 if (typeof(this.options.description) == 'string')
2901 $('<div />')
2902 .addClass('cbi-value-description')
2903 .text(this.options.description)
2904 .appendTo(i.top);
2905
2906 return i.top;
2907 },
2908
2909 ucipath: function(sid)
2910 {
2911 return {
2912 config: (this.options.uci_package || this.map.uci_package),
2913 section: (this.options.uci_section || sid),
2914 option: (this.options.uci_option || this.name)
2915 };
2916 },
2917
2918 ucivalue: function(sid)
2919 {
2920 var uci = this.ucipath(sid);
2921 var val = this.map.get(uci.config, uci.section, uci.option);
2922
2923 if (typeof(val) == 'undefined')
2924 return this.options.initial;
2925
2926 return val;
2927 },
2928
2929 formvalue: function(sid)
2930 {
2931 var v = $('#' + this.id(sid)).val();
2932 return (v === '') ? undefined : v;
2933 },
2934
2935 textvalue: function(sid)
2936 {
2937 var v = this.formvalue(sid);
2938
2939 if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
2940 v = this.ucivalue(sid);
2941
2942 if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length))
2943 v = this.options.placeholder;
2944
2945 if (typeof(v) == 'undefined' || v === '')
2946 return undefined;
2947
2948 if (typeof(v) == 'string' && $.isArray(this.choices))
2949 {
2950 for (var i = 0; i < this.choices.length; i++)
2951 if (v === this.choices[i][0])
2952 return this.choices[i][1];
2953 }
2954 else if (v === true)
2955 return _luci2.tr('yes');
2956 else if (v === false)
2957 return _luci2.tr('no');
2958 else if ($.isArray(v))
2959 return v.join(', ');
2960
2961 return v;
2962 },
2963
2964 changed: function(sid)
2965 {
2966 var a = this.ucivalue(sid);
2967 var b = this.formvalue(sid);
2968
2969 if (typeof(a) != typeof(b))
2970 return true;
2971
2972 if (typeof(a) == 'object')
2973 {
2974 if (a.length != b.length)
2975 return true;
2976
2977 for (var i = 0; i < a.length; i++)
2978 if (a[i] != b[i])
2979 return true;
2980
2981 return false;
2982 }
2983
2984 return (a != b);
2985 },
2986
2987 save: function(sid)
2988 {
2989 var uci = this.ucipath(sid);
2990
2991 if (this.instance[sid].disabled)
2992 {
2993 if (!this.options.keep)
2994 return this.map.set(uci.config, uci.section, uci.option, undefined);
2995
2996 return false;
2997 }
2998
2999 var chg = this.changed(sid);
3000 var val = this.formvalue(sid);
3001
3002 if (chg)
3003 this.map.set(uci.config, uci.section, uci.option, val);
3004
3005 return chg;
3006 },
3007
3008 validator: function(sid, elem, multi)
3009 {
3010 if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
3011 return elem;
3012
3013 var vstack;
3014 if (typeof(this.options.datatype) == 'string')
3015 {
3016 try {
3017 vstack = _luci2.cbi.validation.compile(this.options.datatype);
3018 } catch(e) { };
3019 }
3020 else if (typeof(this.options.datatype) == 'function')
3021 {
3022 var vfunc = this.options.datatype;
3023 vstack = [ function(elem) {
3024 var rv = vfunc(this, elem);
3025 if (rv !== true)
3026 validation.message = rv;
3027 return (rv === true);
3028 }, [ elem ] ];
3029 }
3030
3031 var evdata = {
3032 self: this,
3033 sid: sid,
3034 elem: elem,
3035 multi: multi,
3036 inst: this.instance[sid],
3037 opt: this.options.optional
3038 };
3039
3040 var validator = function(ev)
3041 {
3042 var d = ev.data;
3043 var rv = true;
3044 var val = d.elem.val();
3045
3046 if (vstack && typeof(vstack[0]) == 'function')
3047 {
3048 delete validation.message;
3049
3050 if ((val.length == 0 && !d.opt))
3051 {
3052 d.elem.addClass('error');
3053 d.inst.top.addClass('error');
3054 d.inst.error.text(_luci2.tr('Field must not be empty'));
3055 rv = false;
3056 }
3057 else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
3058 {
3059 d.elem.addClass('error');
3060 d.inst.top.addClass('error');
3061 d.inst.error.text(validation.message.format.apply(validation.message, vstack[1]));
3062 rv = false;
3063 }
3064 else
3065 {
3066 d.elem.removeClass('error');
3067
3068 if (d.multi && d.inst.widget.find('input.error, select.error').length > 0)
3069 {
3070 rv = false;
3071 }
3072 else
3073 {
3074 d.inst.top.removeClass('error');
3075 d.inst.error.text('');
3076 }
3077 }
3078 }
3079
3080 if (rv)
3081 {
3082 for (var field in d.self.rdependency)
3083 d.self.rdependency[field].toggle(d.sid);
3084 }
3085
3086 return rv;
3087 };
3088
3089 if (elem.prop('tagName') == 'SELECT')
3090 {
3091 elem.change(evdata, validator);
3092 }
3093 else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
3094 {
3095 elem.click(evdata, validator);
3096 elem.blur(evdata, validator);
3097 }
3098 else
3099 {
3100 elem.keyup(evdata, validator);
3101 elem.blur(evdata, validator);
3102 }
3103
3104 elem.attr('cbi-validate', true).on('validate', evdata, validator);
3105
3106 return elem;
3107 },
3108
3109 validate: function(sid)
3110 {
3111 var i = this.instance[sid];
3112
3113 i.widget.find('[cbi-validate]').trigger('validate');
3114
3115 return (i.disabled || i.error.text() == '');
3116 },
3117
3118 depends: function(d, v)
3119 {
3120 var dep;
3121
3122 if ($.isArray(d))
3123 {
3124 dep = { };
3125 for (var i = 0; i < d.length; i++)
3126 {
3127 if (typeof(d[i]) == 'string')
3128 dep[d[i]] = true;
3129 else if (d[i] instanceof AbstractValue)
3130 dep[d[i].name] = true;
3131 }
3132 }
3133 else if (d instanceof AbstractValue)
3134 {
3135 dep = { };
3136 dep[d.name] = (typeof(v) == 'undefined') ? true : v;
3137 }
3138 else if (typeof(d) == 'object')
3139 {
3140 dep = d;
3141 }
3142 else if (typeof(d) == 'string')
3143 {
3144 dep = { };
3145 dep[d] = (typeof(v) == 'undefined') ? true : v;
3146 }
3147
3148 if (!dep || $.isEmptyObject(dep))
3149 return this;
3150
3151 for (var field in dep)
3152 {
3153 var f = this.section.fields[field];
3154 if (f)
3155 f.rdependency[this.name] = this;
3156 else
3157 delete dep[field];
3158 }
3159
3160 if ($.isEmptyObject(dep))
3161 return this;
3162
3163 this.dependencies.push(dep);
3164
3165 return this;
3166 },
3167
3168 toggle: function(sid)
3169 {
3170 var d = this.dependencies;
3171 var i = this.instance[sid];
3172
3173 if (!d.length)
3174 return true;
3175
3176 for (var n = 0; n < d.length; n++)
3177 {
3178 var rv = true;
3179
3180 for (var field in d[n])
3181 {
3182 var val = this.section.fields[field].formvalue(sid);
3183 var cmp = d[n][field];
3184
3185 if (typeof(cmp) == 'boolean')
3186 {
3187 if (cmp == (typeof(val) == 'undefined' || val === '' || val === false))
3188 {
3189 rv = false;
3190 break;
3191 }
3192 }
3193 else if (typeof(cmp) == 'string')
3194 {
3195 if (val != cmp)
3196 {
3197 rv = false;
3198 break;
3199 }
3200 }
3201 else if (typeof(cmp) == 'function')
3202 {
3203 if (!cmp(val))
3204 {
3205 rv = false;
3206 break;
3207 }
3208 }
3209 else if (cmp instanceof RegExp)
3210 {
3211 if (!cmp.test(val))
3212 {
3213 rv = false;
3214 break;
3215 }
3216 }
3217 }
3218
3219 if (rv)
3220 {
3221 if (i.disabled)
3222 {
3223 i.disabled = false;
3224 i.top.fadeIn();
3225 }
3226
3227 return true;
3228 }
3229 }
3230
3231 if (!i.disabled)
3232 {
3233 i.disabled = true;
3234 i.top.is(':visible') ? i.top.fadeOut() : i.top.hide();
3235 }
3236
3237 return false;
3238 }
3239 });
3240
3241 this.cbi.CheckboxValue = AbstractValue.extend({
3242 widget: function(sid)
3243 {
3244 var o = this.options;
3245
3246 if (typeof(o.enabled) == 'undefined') o.enabled = '1';
3247 if (typeof(o.disabled) == 'undefined') o.disabled = '0';
3248
3249 var i = $('<input />')
3250 .attr('id', this.id(sid))
3251 .attr('type', 'checkbox')
3252 .prop('checked', this.ucivalue(sid));
3253
3254 return this.validator(sid, i);
3255 },
3256
3257 ucivalue: function(sid)
3258 {
3259 var v = this.callSuper('ucivalue', sid);
3260
3261 if (typeof(v) == 'boolean')
3262 return v;
3263
3264 return (v == this.options.enabled);
3265 },
3266
3267 formvalue: function(sid)
3268 {
3269 var v = $('#' + this.id(sid)).prop('checked');
3270
3271 if (typeof(v) == 'undefined')
3272 return !!this.options.initial;
3273
3274 return v;
3275 },
3276
3277 save: function(sid)
3278 {
3279 var uci = this.ucipath(sid);
3280
3281 if (this.instance[sid].disabled)
3282 {
3283 if (!this.options.keep)
3284 return this.map.set(uci.config, uci.section, uci.option, undefined);
3285
3286 return false;
3287 }
3288
3289 var chg = this.changed(sid);
3290 var val = this.formvalue(sid);
3291
3292 if (chg)
3293 {
3294 val = val ? this.options.enabled : this.options.disabled;
3295
3296 if (this.options.optional && val == this.options.initial)
3297 this.map.set(uci.config, uci.section, uci.option, undefined);
3298 else
3299 this.map.set(uci.config, uci.section, uci.option, val);
3300 }
3301
3302 return chg;
3303 }
3304 });
3305
3306 this.cbi.InputValue = AbstractValue.extend({
3307 widget: function(sid)
3308 {
3309 var i = $('<input />')
3310 .attr('id', this.id(sid))
3311 .attr('type', 'text')
3312 .attr('placeholder', this.options.placeholder)
3313 .val(this.ucivalue(sid));
3314
3315 return this.validator(sid, i);
3316 }
3317 });
3318
3319 this.cbi.PasswordValue = AbstractValue.extend({
3320 widget: function(sid)
3321 {
3322 var i = $('<input />')
3323 .attr('id', this.id(sid))
3324 .attr('type', 'password')
3325 .attr('placeholder', this.options.placeholder)
3326 .val(this.ucivalue(sid));
3327
3328 var t = $('<img />')
3329 .attr('src', _luci2.globals.resource + '/icons/cbi/reload.gif')
3330 .attr('title', _luci2.tr('Reveal or hide password'))
3331 .addClass('cbi-button')
3332 .click(function(ev) {
3333 var i = $(this).prev();
3334 var t = i.attr('type');
3335 i.attr('type', (t == 'password') ? 'text' : 'password');
3336 i = t = null;
3337 });
3338
3339 this.validator(sid, i);
3340
3341 return $('<div />')
3342 .addClass('cbi-input-password')
3343 .append(i)
3344 .append(t);
3345 }
3346 });
3347
3348 this.cbi.ListValue = AbstractValue.extend({
3349 widget: function(sid)
3350 {
3351 var s = $('<select />');
3352
3353 if (this.options.optional)
3354 $('<option />')
3355 .attr('value', '')
3356 .text(_luci2.tr('-- Please choose --'))
3357 .appendTo(s);
3358
3359 if (this.choices)
3360 for (var i = 0; i < this.choices.length; i++)
3361 $('<option />')
3362 .attr('value', this.choices[i][0])
3363 .text(this.choices[i][1])
3364 .appendTo(s);
3365
3366 s.attr('id', this.id(sid)).val(this.ucivalue(sid));
3367
3368 return this.validator(sid, s);
3369 },
3370
3371 value: function(k, v)
3372 {
3373 if (!this.choices)
3374 this.choices = [ ];
3375
3376 this.choices.push([k, v || k]);
3377 return this;
3378 }
3379 });
3380
3381 this.cbi.MultiValue = this.cbi.ListValue.extend({
3382 widget: function(sid)
3383 {
3384 var v = this.ucivalue(sid);
3385 var t = $('<div />').attr('id', this.id(sid));
3386
3387 if (!$.isArray(v))
3388 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
3389
3390 var s = { };
3391 for (var i = 0; i < v.length; i++)
3392 s[v[i]] = true;
3393
3394 if (this.choices)
3395 for (var i = 0; i < this.choices.length; i++)
3396 {
3397 $('<label />')
3398 .append($('<input />')
3399 .addClass('cbi-input-checkbox')
3400 .attr('type', 'checkbox')
3401 .attr('value', this.choices[i][0])
3402 .prop('checked', s[this.choices[i][0]]))
3403 .append(this.choices[i][1])
3404 .appendTo(t);
3405
3406 $('<br />')
3407 .appendTo(t);
3408 }
3409
3410 return t;
3411 },
3412
3413 formvalue: function(sid)
3414 {
3415 var rv = [ ];
3416 var fields = $('#' + this.id(sid) + ' > label > input');
3417
3418 for (var i = 0; i < fields.length; i++)
3419 if (fields[i].checked)
3420 rv.push(fields[i].getAttribute('value'));
3421
3422 return rv;
3423 },
3424
3425 textvalue: function(sid)
3426 {
3427 var v = this.formvalue(sid);
3428 var c = { };
3429
3430 if (this.choices)
3431 for (var i = 0; i < this.choices.length; i++)
3432 c[this.choices[i][0]] = this.choices[i][1];
3433
3434 var t = [ ];
3435
3436 for (var i = 0; i < v.length; i++)
3437 t.push(c[v[i]] || v[i]);
3438
3439 return t.join(', ');
3440 }
3441 });
3442
3443 this.cbi.ComboBox = AbstractValue.extend({
3444 _change: function(ev)
3445 {
3446 var s = ev.target;
3447 var self = ev.data.self;
3448
3449 if (s.selectedIndex == (s.options.length - 1))
3450 {
3451 ev.data.select.hide();
3452 ev.data.input.show().focus();
3453
3454 var v = ev.data.input.val();
3455 ev.data.input.val(' ');
3456 ev.data.input.val(v);
3457 }
3458 else if (self.options.optional && s.selectedIndex == 0)
3459 {
3460 ev.data.input.val('');
3461 }
3462 else
3463 {
3464 ev.data.input.val(ev.data.select.val());
3465 }
3466 },
3467
3468 _blur: function(ev)
3469 {
3470 var seen = false;
3471 var val = this.value;
3472 var self = ev.data.self;
3473
3474 ev.data.select.empty();
3475
3476 if (self.options.optional)
3477 $('<option />')
3478 .attr('value', '')
3479 .text(_luci2.tr('-- please choose --'))
3480 .appendTo(ev.data.select);
3481
3482 if (self.choices)
3483 for (var i = 0; i < self.choices.length; i++)
3484 {
3485 if (self.choices[i][0] == val)
3486 seen = true;
3487
3488 $('<option />')
3489 .attr('value', self.choices[i][0])
3490 .text(self.choices[i][1])
3491 .appendTo(ev.data.select);
3492 }
3493
3494 if (!seen && val != '')
3495 $('<option />')
3496 .attr('value', val)
3497 .text(val)
3498 .appendTo(ev.data.select);
3499
3500 $('<option />')
3501 .attr('value', ' ')
3502 .text(_luci2.tr('-- custom --'))
3503 .appendTo(ev.data.select);
3504
3505 ev.data.input.hide();
3506 ev.data.select.val(val).show().focus();
3507 },
3508
3509 _enter: function(ev)
3510 {
3511 if (ev.which != 13)
3512 return true;
3513
3514 ev.preventDefault();
3515 ev.data.self._blur(ev);
3516 return false;
3517 },
3518
3519 widget: function(sid)
3520 {
3521 var d = $('<div />')
3522 .attr('id', this.id(sid));
3523
3524 var t = $('<input />')
3525 .attr('type', 'text')
3526 .hide()
3527 .appendTo(d);
3528
3529 var s = $('<select />')
3530 .appendTo(d);
3531
3532 var evdata = {
3533 self: this,
3534 input: this.validator(sid, t),
3535 select: this.validator(sid, s)
3536 };
3537
3538 s.change(evdata, this._change);
3539 t.blur(evdata, this._blur);
3540 t.keydown(evdata, this._enter);
3541
3542 t.val(this.ucivalue(sid));
3543 t.blur();
3544
3545 return d;
3546 },
3547
3548 value: function(k, v)
3549 {
3550 if (!this.choices)
3551 this.choices = [ ];
3552
3553 this.choices.push([k, v || k]);
3554 return this;
3555 },
3556
3557 formvalue: function(sid)
3558 {
3559 var v = $('#' + this.id(sid)).children('input').val();
3560 return (v == '') ? undefined : v;
3561 }
3562 });
3563
3564 this.cbi.DynamicList = this.cbi.ComboBox.extend({
3565 _redraw: function(focus, add, del, s)
3566 {
3567 var v = s.values || [ ];
3568 delete s.values;
3569
3570 $(s.parent).children('input').each(function(i) {
3571 if (i != del)
3572 v.push(this.value || '');
3573 });
3574
3575 $(s.parent).empty();
3576
3577 if (add >= 0)
3578 {
3579 focus = add + 1;
3580 v.splice(focus, 0, '');
3581 }
3582 else if (v.length == 0)
3583 {
3584 focus = 0;
3585 v.push('');
3586 }
3587
3588 for (var i = 0; i < v.length; i++)
3589 {
3590 var evdata = {
3591 sid: s.sid,
3592 self: s.self,
3593 parent: s.parent,
3594 index: i
3595 };
3596
3597 if (this.choices)
3598 {
3599 var txt = $('<input />')
3600 .attr('type', 'text')
3601 .hide()
3602 .appendTo(s.parent);
3603
3604 var sel = $('<select />')
3605 .appendTo(s.parent);
3606
3607 evdata.input = this.validator(s.sid, txt, true);
3608 evdata.select = this.validator(s.sid, sel, true);
3609
3610 sel.change(evdata, this._change);
3611 txt.blur(evdata, this._blur);
3612 txt.keydown(evdata, this._keydown);
3613
3614 txt.val(v[i]);
3615 txt.blur();
3616
3617 if (i == focus || -(i+1) == focus)
3618 sel.focus();
3619
3620 sel = txt = null;
3621 }
3622 else
3623 {
3624 var f = $('<input />')
3625 .attr('type', 'text')
3626 .attr('index', i)
3627 .attr('placeholder', (i == 0) ? this.options.placeholder : '')
3628 .addClass('cbi-input-text')
3629 .keydown(evdata, this._keydown)
3630 .keypress(evdata, this._keypress)
3631 .val(v[i]);
3632
3633 f.appendTo(s.parent);
3634
3635 if (i == focus)
3636 {
3637 f.focus();
3638 }
3639 else if (-(i+1) == focus)
3640 {
3641 f.focus();
3642
3643 /* force cursor to end */
3644 var val = f.val();
3645 f.val(' ');
3646 f.val(val);
3647 }
3648
3649 evdata.input = this.validator(s.sid, f, true);
3650
3651 f = null;
3652 }
3653
3654 $('<img />')
3655 .attr('src', _luci2.globals.resource + ((i+1) < v.length ? '/icons/cbi/remove.gif' : '/icons/cbi/add.gif'))
3656 .attr('title', (i+1) < v.length ? _luci2.tr('Remove entry') : _luci2.tr('Add entry'))
3657 .addClass('cbi-button')
3658 .click(evdata, this._btnclick)
3659 .appendTo(s.parent);
3660
3661 $('<br />')
3662 .appendTo(s.parent);
3663
3664 evdata = null;
3665 }
3666
3667 s = null;
3668 },
3669
3670 _keypress: function(ev)
3671 {
3672 switch (ev.which)
3673 {
3674 /* backspace, delete */
3675 case 8:
3676 case 46:
3677 if (ev.data.input.val() == '')
3678 {
3679 ev.preventDefault();
3680 return false;
3681 }
3682
3683 return true;
3684
3685 /* enter, arrow up, arrow down */
3686 case 13:
3687 case 38:
3688 case 40:
3689 ev.preventDefault();
3690 return false;
3691 }
3692
3693 return true;
3694 },
3695
3696 _keydown: function(ev)
3697 {
3698 var input = ev.data.input;
3699
3700 switch (ev.which)
3701 {
3702 /* backspace, delete */
3703 case 8:
3704 case 46:
3705 if (input.val().length == 0)
3706 {
3707 ev.preventDefault();
3708
3709 var index = ev.data.index;
3710 var focus = index;
3711
3712 if (ev.which == 8)
3713 focus = -focus;
3714
3715 ev.data.self._redraw(focus, -1, index, ev.data);
3716 return false;
3717 }
3718
3719 break;
3720
3721 /* enter */
3722 case 13:
3723 ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
3724 break;
3725
3726 /* arrow up */
3727 case 38:
3728 var prev = input.prevAll('input:first');
3729 if (prev.is(':visible'))
3730 prev.focus();
3731 else
3732 prev.next('select').focus();
3733 break;
3734
3735 /* arrow down */
3736 case 40:
3737 var next = input.nextAll('input:first');
3738 if (next.is(':visible'))
3739 next.focus();
3740 else
3741 next.next('select').focus();
3742 break;
3743 }
3744
3745 return true;
3746 },
3747
3748 _btnclick: function(ev)
3749 {
3750 if (!this.getAttribute('disabled'))
3751 {
3752 if (ev.target.src.indexOf('remove') > -1)
3753 {
3754 var index = ev.data.index;
3755 ev.data.self._redraw(-index, -1, index, ev.data);
3756 }
3757 else
3758 {
3759 ev.data.self._redraw(NaN, ev.data.index, -1, ev.data);
3760 }
3761 }
3762
3763 return false;
3764 },
3765
3766 widget: function(sid)
3767 {
3768 this.options.optional = true;
3769
3770 var v = this.ucivalue(sid);
3771
3772 if (!$.isArray(v))
3773 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
3774
3775 var d = $('<div />')
3776 .attr('id', this.id(sid))
3777 .addClass('cbi-input-dynlist');
3778
3779 this._redraw(NaN, -1, -1, {
3780 self: this,
3781 parent: d[0],
3782 values: v,
3783 sid: sid
3784 });
3785
3786 return d;
3787 },
3788
3789 ucivalue: function(sid)
3790 {
3791 var v = this.callSuper('ucivalue', sid);
3792
3793 if (!$.isArray(v))
3794 v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ];
3795
3796 return v;
3797 },
3798
3799 formvalue: function(sid)
3800 {
3801 var rv = [ ];
3802 var fields = $('#' + this.id(sid) + ' > input');
3803
3804 for (var i = 0; i < fields.length; i++)
3805 if (typeof(fields[i].value) == 'string' && fields[i].value.length)
3806 rv.push(fields[i].value);
3807
3808 return rv;
3809 }
3810 });
3811
3812 this.cbi.DummyValue = AbstractValue.extend({
3813 widget: function(sid)
3814 {
3815 return $('<div />')
3816 .addClass('cbi-value-dummy')
3817 .attr('id', this.id(sid))
3818 .html(this.ucivalue(sid));
3819 },
3820
3821 formvalue: function(sid)
3822 {
3823 return this.ucivalue(sid);
3824 }
3825 });
3826
3827 this.cbi.NetworkList = AbstractValue.extend({
3828 load: function(sid)
3829 {
3830 var self = this;
3831
3832 if (!self.interfaces)
3833 {
3834 self.interfaces = [ ];
3835 return _luci2.network.getNetworkStatus(function(ifaces) {
3836 self.interfaces = ifaces;
3837 self = null;
3838 });
3839 }
3840
3841 return undefined;
3842 },
3843
3844 _device_icon: function(dev)
3845 {
3846 var type = 'ethernet';
3847 var desc = _luci2.tr('Ethernet device');
3848
3849 if (dev.type == 'IP tunnel')
3850 {
3851 type = 'tunnel';
3852 desc = _luci2.tr('Tunnel interface');
3853 }
3854 else if (dev['bridge-members'])
3855 {
3856 type = 'bridge';
3857 desc = _luci2.tr('Bridge');
3858 }
3859 else if (dev.wireless)
3860 {
3861 type = 'wifi';
3862 desc = _luci2.tr('Wireless Network');
3863 }
3864 else if (dev.name.indexOf('.') > 0)
3865 {
3866 type = 'vlan';
3867 desc = _luci2.tr('VLAN interface');
3868 }
3869
3870 return $('<img />')
3871 .attr('src', _luci2.globals.resource + '/icons/' + type + (dev.up ? '' : '_disabled') + '.png')
3872 .attr('title', '%s (%s)'.format(desc, dev.name));
3873 },
3874
3875 widget: function(sid)
3876 {
3877 var id = this.id(sid);
3878 var ul = $('<ul />')
3879 .attr('id', id)
3880 .addClass('cbi-input-networks');
3881
3882 var itype = this.options.multiple ? 'checkbox' : 'radio';
3883 var value = this.ucivalue(sid);
3884 var check = { };
3885
3886 if (!this.options.multiple)
3887 check[value] = true;
3888 else
3889 for (var i = 0; i < value.length; i++)
3890 check[value[i]] = true;
3891
3892 if (this.interfaces)
3893 {
3894 for (var i = 0; i < this.interfaces.length; i++)
3895 {
3896 var iface = this.interfaces[i];
3897 var badge = $('<span />')
3898 .addClass('ifacebadge')
3899 .text('%s: '.format(iface.name));
3900
3901 if (iface.subdevices)
3902 for (var j = 0; j < iface.subdevices.length; j++)
3903 badge.append(this._device_icon(iface.subdevices[j]));
3904 else if (iface.device)
3905 badge.append(this._device_icon(iface.device));
3906 else
3907 badge.append($('<em />').text(_luci2.tr('(No devices attached)')));
3908
3909 $('<li />')
3910 .append($('<label />')
3911 .append($('<input />')
3912 .attr('name', itype + id)
3913 .attr('type', itype)
3914 .attr('value', iface.name)
3915 .prop('checked', !!check[iface.name])
3916 .addClass('cbi-input-' + itype))
3917 .append(badge))
3918 .appendTo(ul);
3919 }
3920 }
3921
3922 if (!this.options.multiple)
3923 {
3924 $('<li />')
3925 .append($('<label />')
3926 .append($('<input />')
3927 .attr('name', itype + id)
3928 .attr('type', itype)
3929 .attr('value', '')
3930 .prop('checked', !value)
3931 .addClass('cbi-input-' + itype))
3932 .append(_luci2.tr('unspecified')))
3933 .appendTo(ul);
3934 }
3935
3936 return ul;
3937 },
3938
3939 ucivalue: function(sid)
3940 {
3941 var v = this.callSuper('ucivalue', sid);
3942
3943 if (!this.options.multiple)
3944 {
3945 if ($.isArray(v))
3946 {
3947 return v[0];
3948 }
3949 else if (typeof(v) == 'string')
3950 {
3951 v = v.match(/\S+/);
3952 return v ? v[0] : undefined;
3953 }
3954
3955 return v;
3956 }
3957 else
3958 {
3959 if (typeof(v) == 'string')
3960 v = v.match(/\S+/g);
3961
3962 return v || [ ];
3963 }
3964 },
3965
3966 formvalue: function(sid)
3967 {
3968 var inputs = $('#' + this.id(sid) + ' input');
3969
3970 if (!this.options.multiple)
3971 {
3972 for (var i = 0; i < inputs.length; i++)
3973 if (inputs[i].checked && inputs[i].value !== '')
3974 return inputs[i].value;
3975
3976 return undefined;
3977 }
3978
3979 var rv = [ ];
3980
3981 for (var i = 0; i < inputs.length; i++)
3982 if (inputs[i].checked)
3983 rv.push(inputs[i].value);
3984
3985 return rv.length ? rv : undefined;
3986 }
3987 });
3988
3989
3990 var AbstractSection = AbstractWidget.extend({
3991 id: function()
3992 {
3993 var s = [ arguments[0], this.map.uci_package, this.uci_type ];
3994
3995 for (var i = 1; i < arguments.length; i++)
3996 s.push(arguments[i].replace(/\./g, '_'));
3997
3998 return s.join('_');
3999 },
4000
4001 option: function(widget, name, options)
4002 {
4003 if (this.tabs.length == 0)
4004 this.tab({ id: '__default__', selected: true });
4005
4006 return this.taboption('__default__', widget, name, options);
4007 },
4008
4009 tab: function(options)
4010 {
4011 if (options.selected)
4012 this.tabs.selected = this.tabs.length;
4013
4014 this.tabs.push({
4015 id: options.id,
4016 caption: options.caption,
4017 description: options.description,
4018 fields: [ ],
4019 li: { }
4020 });
4021 },
4022
4023 taboption: function(tabid, widget, name, options)
4024 {
4025 var tab;
4026 for (var i = 0; i < this.tabs.length; i++)
4027 {
4028 if (this.tabs[i].id == tabid)
4029 {
4030 tab = this.tabs[i];
4031 break;
4032 }
4033 }
4034
4035 if (!tab)
4036 throw 'Cannot append to unknown tab ' + tabid;
4037
4038 var w = widget ? new widget(name, options) : null;
4039
4040 if (!(w instanceof AbstractValue))
4041 throw 'Widget must be an instance of AbstractValue';
4042
4043 w.section = this;
4044 w.map = this.map;
4045
4046 this.fields[name] = w;
4047 tab.fields.push(w);
4048
4049 return w;
4050 },
4051
4052 ucipackages: function(pkg)
4053 {
4054 for (var i = 0; i < this.tabs.length; i++)
4055 for (var j = 0; j < this.tabs[i].fields.length; j++)
4056 if (this.tabs[i].fields[j].options.uci_package)
4057 pkg[this.tabs[i].fields[j].options.uci_package] = true;
4058 },
4059
4060 formvalue: function()
4061 {
4062 var rv = { };
4063
4064 this.sections(function(s) {
4065 var sid = s['.name'];
4066 var sv = rv[sid] || (rv[sid] = { });
4067
4068 for (var i = 0; i < this.tabs.length; i++)
4069 for (var j = 0; j < this.tabs[i].fields.length; j++)
4070 {
4071 var val = this.tabs[i].fields[j].formvalue(sid);
4072 sv[this.tabs[i].fields[j].name] = val;
4073 }
4074 });
4075
4076 return rv;
4077 },
4078
4079 validate: function(sid)
4080 {
4081 var rv = true;
4082
4083 if (!sid)
4084 {
4085 var as = this.sections();
4086 for (var i = 0; i < as.length; i++)
4087 if (!this.validate(as[i]['.name']))
4088 rv = false;
4089 return rv;
4090 }
4091
4092 var inst = this.instance[sid];
4093 var sv = rv[sid] || (rv[sid] = { });
4094
4095 var invals = 0;
4096 var legend = $('#' + this.id('sort', sid)).find('legend:first');
4097
4098 legend.children('span').detach();
4099
4100 for (var i = 0; i < this.tabs.length; i++)
4101 {
4102 var inval = 0;
4103 var tab = $('#' + this.id('tabhead', sid, this.tabs[i].id));
4104
4105 tab.children('span').detach();
4106
4107 for (var j = 0; j < this.tabs[i].fields.length; j++)
4108 if (!this.tabs[i].fields[j].validate(sid))
4109 inval++;
4110
4111 if (inval > 0)
4112 {
4113 $('<span />')
4114 .addClass('badge')
4115 .attr('title', _luci2.tr('%d Errors'.format(inval)))
4116 .text(inval)
4117 .appendTo(tab);
4118
4119 invals += inval;
4120 tab = null;
4121 rv = false;
4122 }
4123 }
4124
4125 if (invals > 0)
4126 $('<span />')
4127 .addClass('badge')
4128 .attr('title', _luci2.tr('%d Errors'.format(invals)))
4129 .text(invals)
4130 .appendTo(legend);
4131
4132 return rv;
4133 }
4134 });
4135
4136 this.cbi.TypedSection = AbstractSection.extend({
4137 init: function(uci_type, options)
4138 {
4139 this.uci_type = uci_type;
4140 this.options = options;
4141 this.tabs = [ ];
4142 this.fields = { };
4143 this.active_panel = 0;
4144 this.active_tab = { };
4145 },
4146
4147 filter: function(section)
4148 {
4149 return true;
4150 },
4151
4152 sections: function(cb)
4153 {
4154 var s1 = this.map.ucisections(this.map.uci_package);
4155 var s2 = [ ];
4156
4157 for (var i = 0; i < s1.length; i++)
4158 if (s1[i]['.type'] == this.uci_type)
4159 if (this.filter(s1[i]))
4160 s2.push(s1[i]);
4161
4162 if (typeof(cb) == 'function')
4163 for (var i = 0; i < s2.length; i++)
4164 cb.apply(this, [ s2[i] ]);
4165
4166 return s2;
4167 },
4168
4169 add: function(name)
4170 {
4171 this.map.add(this.map.uci_package, this.uci_type, name);
4172 },
4173
4174 remove: function(sid)
4175 {
4176 this.map.remove(this.map.uci_package, sid);
4177 },
4178
4179 _add: function(ev)
4180 {
4181 var addb = $(this);
4182 var name = undefined;
4183 var self = ev.data.self;
4184
4185 if (addb.prev().prop('nodeName') == 'INPUT')
4186 name = addb.prev().val();
4187
4188 if (addb.prop('disabled') || name === '')
4189 return;
4190
4191 self.active_panel = -1;
4192 self.map.save();
4193 self.add(name);
4194 self.map.redraw();
4195 },
4196
4197 _remove: function(ev)
4198 {
4199 var self = ev.data.self;
4200 var sid = ev.data.sid;
4201
4202 self.map.save();
4203 self.remove(sid);
4204 self.map.redraw();
4205
4206 ev.stopPropagation();
4207 },
4208
4209 _sid: function(ev)
4210 {
4211 var self = ev.data.self;
4212 var text = $(this);
4213 var addb = text.next();
4214 var errt = addb.next();
4215 var name = text.val();
4216 var used = false;
4217
4218 if (!/^[a-zA-Z0-9_]*$/.test(name))
4219 {
4220 errt.text(_luci2.tr('Invalid section name')).show();
4221 text.addClass('error');
4222 addb.prop('disabled', true);
4223 return false;
4224 }
4225
4226 for (var sid in self.map.uci.values[self.map.uci_package])
4227 if (sid == name)
4228 {
4229 used = true;
4230 break;
4231 }
4232
4233 for (var sid in self.map.uci.creates[self.map.uci_package])
4234 if (sid == name)
4235 {
4236 used = true;
4237 break;
4238 }
4239
4240 if (used)
4241 {
4242 errt.text(_luci2.tr('Name already used')).show();
4243 text.addClass('error');
4244 addb.prop('disabled', true);
4245 return false;
4246 }
4247
4248 errt.text('').hide();
4249 text.removeClass('error');
4250 addb.prop('disabled', false);
4251 return true;
4252 },
4253
4254 teaser: function(sid)
4255 {
4256 var tf = this.teaser_fields;
4257
4258 if (!tf)
4259 {
4260 tf = this.teaser_fields = [ ];
4261
4262 if ($.isArray(this.options.teasers))
4263 {
4264 for (var i = 0; i < this.options.teasers.length; i++)
4265 {
4266 var f = this.options.teasers[i];
4267 if (f instanceof AbstractValue)
4268 tf.push(f);
4269 else if (typeof(f) == 'string' && this.fields[f] instanceof AbstractValue)
4270 tf.push(this.fields[f]);
4271 }
4272 }
4273 else
4274 {
4275 for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
4276 for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
4277 tf.push(this.tabs[i].fields[j]);
4278 }
4279 }
4280
4281 var t = '';
4282
4283 for (var i = 0; i < tf.length; i++)
4284 {
4285 if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
4286 continue;
4287
4288 var n = tf[i].options.caption || tf[i].name;
4289 var v = tf[i].textvalue(sid);
4290
4291 if (typeof(v) == 'undefined')
4292 continue;
4293
4294 t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
4295 }
4296
4297 return t;
4298 },
4299
4300 _render_add: function()
4301 {
4302 var text = _luci2.tr('Add section');
4303 var ttip = _luci2.tr('Create new section...');
4304
4305 if ($.isArray(this.options.add_caption))
4306 text = this.options.add_caption[0], ttip = this.options.add_caption[1];
4307 else if (typeof(this.options.add_caption) == 'string')
4308 text = this.options.add_caption, ttip = '';
4309
4310 var add = $('<div />').addClass('cbi-section-add');
4311
4312 if (this.options.anonymous === false)
4313 {
4314 $('<input />')
4315 .addClass('cbi-input-text')
4316 .attr('type', 'text')
4317 .attr('placeholder', ttip)
4318 .blur({ self: this }, this._sid)
4319 .keyup({ self: this }, this._sid)
4320 .appendTo(add);
4321
4322 $('<img />')
4323 .attr('src', _luci2.globals.resource + '/icons/cbi/add.gif')
4324 .attr('title', text)
4325 .addClass('cbi-button')
4326 .click({ self: this }, this._add)
4327 .appendTo(add);
4328
4329 $('<div />')
4330 .addClass('cbi-value-error')
4331 .hide()
4332 .appendTo(add);
4333 }
4334 else
4335 {
4336 $('<input />')
4337 .attr('type', 'button')
4338 .addClass('cbi-button')
4339 .addClass('cbi-button-add')
4340 .val(text).attr('title', ttip)
4341 .click({ self: this }, this._add)
4342 .appendTo(add)
4343 }
4344
4345 return add;
4346 },
4347
4348 _render_remove: function(sid)
4349 {
4350 var text = _luci2.tr('Remove');
4351 var ttip = _luci2.tr('Remove this section');
4352
4353 if ($.isArray(this.options.remove_caption))
4354 text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
4355 else if (typeof(this.options.remove_caption) == 'string')
4356 text = this.options.remove_caption, ttip = '';
4357
4358 return $('<input />')
4359 .attr('type', 'button')
4360 .addClass('cbi-button')
4361 .addClass('cbi-button-remove')
4362 .val(text).attr('title', ttip)
4363 .click({ self: this, sid: sid }, this._remove);
4364 },
4365
4366 _render_caption: function(sid)
4367 {
4368 if (typeof(this.options.caption) == 'string')
4369 {
4370 return $('<legend />')
4371 .text(this.options.caption.format(sid));
4372 }
4373 else if (typeof(this.options.caption) == 'function')
4374 {
4375 return $('<legend />')
4376 .text(this.options.caption.call(this, sid));
4377 }
4378
4379 return '';
4380 },
4381
4382 render: function()
4383 {
4384 var allsections = $();
4385 var panel_index = 0;
4386
4387 this.instance = { };
4388
4389 var s = this.sections();
4390
4391 if (s.length == 0)
4392 {
4393 var fieldset = $('<fieldset />')
4394 .addClass('cbi-section');
4395
4396 var head = $('<div />')
4397 .addClass('cbi-section-head')
4398 .appendTo(fieldset);
4399
4400 head.append(this._render_caption(undefined));
4401
4402 if (typeof(this.options.description) == 'string')
4403 {
4404 $('<div />')
4405 .addClass('cbi-section-descr')
4406 .text(this.options.description)
4407 .appendTo(head);
4408 }
4409
4410 allsections = allsections.add(fieldset);
4411 }
4412
4413 for (var i = 0; i < s.length; i++)
4414 {
4415 var sid = s[i]['.name'];
4416 var inst = this.instance[sid] = { tabs: [ ] };
4417
4418 var fieldset = $('<fieldset />')
4419 .attr('id', this.id('sort', sid))
4420 .addClass('cbi-section');
4421
4422 var head = $('<div />')
4423 .addClass('cbi-section-head')
4424 .attr('cbi-section-num', this.index)
4425 .attr('cbi-section-id', sid);
4426
4427 head.append(this._render_caption(sid));
4428
4429 if (typeof(this.options.description) == 'string')
4430 {
4431 $('<div />')
4432 .addClass('cbi-section-descr')
4433 .text(this.options.description)
4434 .appendTo(head);
4435 }
4436
4437 var teaser;
4438 if ((s.length > 1 && this.options.collabsible) || this.map.options.collabsible)
4439 teaser = $('<div />')
4440 .addClass('cbi-section-teaser')
4441 .appendTo(head);
4442
4443 if (this.options.addremove)
4444 $('<div />')
4445 .addClass('cbi-section-remove')
4446 .addClass('right')
4447 .append(this._render_remove(sid))
4448 .appendTo(head);
4449
4450 var body = $('<div />')
4451 .attr('index', panel_index++);
4452
4453 var fields = $('<fieldset />')
4454 .addClass('cbi-section-node');
4455
4456 if (this.tabs.length > 1)
4457 {
4458 var menu = $('<ul />')
4459 .addClass('cbi-tabmenu');
4460
4461 for (var j = 0; j < this.tabs.length; j++)
4462 {
4463 var tabid = this.id('tab', sid, this.tabs[j].id);
4464 var theadid = this.id('tabhead', sid, this.tabs[j].id);
4465
4466 var tabc = $('<div />')
4467 .addClass('cbi-tabcontainer')
4468 .attr('id', tabid)
4469 .attr('index', j);
4470
4471 if (typeof(this.tabs[j].description) == 'string')
4472 {
4473 $('<div />')
4474 .addClass('cbi-tab-descr')
4475 .text(this.tabs[j].description)
4476 .appendTo(tabc);
4477 }
4478
4479 for (var k = 0; k < this.tabs[j].fields.length; k++)
4480 this.tabs[j].fields[k].render(sid).appendTo(tabc);
4481
4482 tabc.appendTo(fields);
4483 tabc = null;
4484
4485 $('<li />').attr('id', theadid).append(
4486 $('<a />')
4487 .text(this.tabs[j].caption.format(this.tabs[j].id))
4488 .attr('href', '#' + tabid)
4489 ).appendTo(menu);
4490 }
4491
4492 menu.appendTo(body);
4493 menu = null;
4494
4495 fields.appendTo(body);
4496 fields = null;
4497
4498 var t = body.tabs({ active: this.active_tab[sid] });
4499
4500 t.on('tabsactivate', { self: this, sid: sid }, function(ev, ui) {
4501 var d = ev.data;
4502 d.self.validate();
4503 d.self.active_tab[d.sid] = parseInt(ui.newPanel.attr('index'));
4504 });
4505 }
4506 else
4507 {
4508 for (var j = 0; j < this.tabs[0].fields.length; j++)
4509 this.tabs[0].fields[j].render(sid).appendTo(fields);
4510
4511 fields.appendTo(body);
4512 fields = null;
4513 }
4514
4515 head.appendTo(fieldset);
4516 head = null;
4517
4518 body.appendTo(fieldset);
4519 body = null;
4520
4521 allsections = allsections.add(fieldset);
4522 fieldset = null;
4523
4524 //this.validate(sid);
4525 //
4526 //if (teaser)
4527 // teaser.append(this.teaser(sid));
4528 }
4529
4530 if (this.options.collabsible && s.length > 1)
4531 {
4532 var a = $('<div />').append(allsections).accordion({
4533 header: '> fieldset > div.cbi-section-head',
4534 heightStyle: 'content',
4535 active: this.active_panel
4536 });
4537
4538 a.on('accordionbeforeactivate', { self: this }, function(ev, ui) {
4539 var h = ui.oldHeader;
4540 var s = ev.data.self;
4541 var i = h.attr('cbi-section-id');
4542
4543 h.children('.cbi-section-teaser').empty().append(s.teaser(i));
4544 s.validate();
4545 });
4546
4547 a.on('accordionactivate', { self: this }, function(ev, ui) {
4548 ev.data.self.active_panel = parseInt(ui.newPanel.attr('index'));
4549 });
4550
4551 if (this.options.sortable)
4552 {
4553 var s = a.sortable({
4554 axis: 'y',
4555 handle: 'div.cbi-section-head'
4556 });
4557
4558 s.on('sortupdate', { self: this, ids: s.sortable('toArray') }, function(ev, ui) {
4559 var sections = [ ];
4560 for (var i = 0; i < ev.data.ids.length; i++)
4561 sections.push(ev.data.ids[i].substring(ev.data.ids[i].lastIndexOf('.') + 1));
4562 _luci2.uci.order(ev.data.self.map.uci_package, sections);
4563 });
4564
4565 s.on('sortstop', function(ev, ui) {
4566 ui.item.children('div.cbi-section-head').triggerHandler('focusout');
4567 });
4568 }
4569
4570 if (this.options.addremove)
4571 this._render_add().appendTo(a);
4572
4573 return a;
4574 }
4575
4576 if (this.options.addremove)
4577 allsections = allsections.add(this._render_add());
4578
4579 return allsections;
4580 },
4581
4582 finish: function()
4583 {
4584 var s = this.sections();
4585
4586 for (var i = 0; i < s.length; i++)
4587 {
4588 var sid = s[i]['.name'];
4589
4590 this.validate(sid);
4591
4592 $('#' + this.id('sort', sid))
4593 .children('.cbi-section-head')
4594 .children('.cbi-section-teaser')
4595 .append(this.teaser(sid));
4596 }
4597 }
4598 });
4599
4600 this.cbi.TableSection = this.cbi.TypedSection.extend({
4601 render: function()
4602 {
4603 var allsections = $();
4604 var panel_index = 0;
4605
4606 this.instance = { };
4607
4608 var s = this.sections();
4609
4610 var fieldset = $('<fieldset />')
4611 .addClass('cbi-section');
4612
4613 fieldset.append(this._render_caption(sid));
4614
4615 if (typeof(this.options.description) == 'string')
4616 {
4617 $('<div />')
4618 .addClass('cbi-section-descr')
4619 .text(this.options.description)
4620 .appendTo(fieldset);
4621 }
4622
4623 var fields = $('<div />')
4624 .addClass('cbi-section-node')
4625 .appendTo(fieldset);
4626
4627 var table = $('<table />')
4628 .addClass('cbi-section-table')
4629 .appendTo(fields);
4630
4631 var thead = $('<thead />')
4632 .append($('<tr />').addClass('cbi-section-table-titles'))
4633 .appendTo(table);
4634
4635 for (var j = 0; j < this.tabs[0].fields.length; j++)
4636 $('<th />')
4637 .addClass('cbi-section-table-cell')
4638 .css('width', this.tabs[0].fields[j].options.width || '')
4639 .append(this.tabs[0].fields[j].options.caption)
4640 .appendTo(thead.children());
4641
4642 if (this.options.sortable)
4643 $('<th />').addClass('cbi-section-table-cell').text(' ').appendTo(thead.children());
4644
4645 if (this.options.addremove !== false)
4646 $('<th />').addClass('cbi-section-table-cell').text(' ').appendTo(thead.children());
4647
4648 var tbody = $('<tbody />')
4649 .appendTo(table);
4650
4651 if (s.length == 0)
4652 {
4653 $('<tr />')
4654 .addClass('cbi-section-table-row')
4655 .append(
4656 $('<td />')
4657 .addClass('cbi-section-table-cell')
4658 .addClass('cbi-section-table-placeholder')
4659 .attr('colspan', thead.children().children().length)
4660 .text(this.options.placeholder || _luci2.tr('This section contains no values yet')))
4661 .appendTo(tbody);
4662 }
4663
4664 for (var i = 0; i < s.length; i++)
4665 {
4666 var sid = s[i]['.name'];
4667 var inst = this.instance[sid] = { tabs: [ ] };
4668
4669 var row = $('<tr />')
4670 .addClass('cbi-section-table-row')
4671 .appendTo(tbody);
4672
4673 for (var j = 0; j < this.tabs[0].fields.length; j++)
4674 {
4675 $('<td />')
4676 .addClass('cbi-section-table-cell')
4677 .css('width', this.tabs[0].fields[j].options.width || '')
4678 .append(this.tabs[0].fields[j].render(sid, true))
4679 .appendTo(row);
4680 }
4681
4682 if (this.options.sortable)
4683 {
4684 $('<td />')
4685 .addClass('cbi-section-table-cell')
4686 .addClass('cbi-section-table-sort')
4687 .append($('<img />').attr('src', _luci2.globals.resource + '/icons/cbi/up.gif').attr('title', _luci2.tr('Drag to sort')))
4688 .append($('<br />'))
4689 .append($('<img />').attr('src', _luci2.globals.resource + '/icons/cbi/down.gif').attr('title', _luci2.tr('Drag to sort')))
4690 .appendTo(row);
4691 }
4692
4693 if (this.options.addremove !== false)
4694 {
4695 $('<td />')
4696 .addClass('cbi-section-table-cell')
4697 .append(this._render_remove(sid))
4698 .appendTo(row);
4699 }
4700
4701 this.validate(sid);
4702
4703 row = null;
4704 }
4705
4706 if (this.options.sortable)
4707 {
4708 var s = tbody.sortable({
4709 handle: 'td.cbi-section-table-sort'
4710 });
4711
4712 s.on('sortupdate', { self: this, ids: s.sortable('toArray') }, function(ev, ui) {
4713 var sections = [ ];
4714 for (var i = 0; i < ev.data.ids.length; i++)
4715 sections.push(ev.data.ids[i].substring(ev.data.ids[i].lastIndexOf('.') + 1));
4716 _luci2.uci.order(ev.data.self.map.uci_package, sections);
4717 });
4718
4719 s.on('sortstop', function(ev, ui) {
4720 ui.item.children('div.cbi-section-head').triggerHandler('focusout');
4721 });
4722 }
4723
4724 if (this.options.addremove)
4725 this._render_add().appendTo(fieldset);
4726
4727 fields = table = thead = tbody = null;
4728
4729 return fieldset;
4730 }
4731 });
4732
4733 this.cbi.NamedSection = this.cbi.TypedSection.extend({
4734 sections: function(cb)
4735 {
4736 var sa = [ ];
4737 var pkg = this.map.uci.values[this.map.uci_package];
4738
4739 for (var s in pkg)
4740 if (pkg[s]['.name'] == this.uci_type)
4741 {
4742 sa.push(pkg[s]);
4743 break;
4744 }
4745
4746 if (typeof(cb) == 'function' && sa.length > 0)
4747 cb.apply(this, [ sa[0] ]);
4748
4749 return sa;
4750 }
4751 });
4752
4753 this.cbi.DummySection = this.cbi.TypedSection.extend({
4754 sections: function(cb)
4755 {
4756 if (typeof(cb) == 'function')
4757 cb.apply(this, [ { '.name': this.uci_type } ]);
4758
4759 return [ { '.name': this.uci_type } ];
4760 }
4761 });
4762
4763 this.cbi.Map = AbstractWidget.extend({
4764 init: function(uci_package, options)
4765 {
4766 var self = this;
4767
4768 this.uci_package = uci_package;
4769 this.sections = [ ];
4770 this.options = _luci2.defaults(options, {
4771 save: function() { },
4772 prepare: function() {
4773 return _luci2.uci.writable(function(writable) {
4774 self.options.readonly = !writable;
4775 });
4776 }
4777 });
4778 },
4779
4780 load: function()
4781 {
4782 this.uci = {
4783 newid: 0,
4784 values: { },
4785 creates: { },
4786 changes: { },
4787 deletes: { }
4788 };
4789
4790 this.active_panel = 0;
4791
4792 var packages = { };
4793
4794 for (var i = 0; i < this.sections.length; i++)
4795 this.sections[i].ucipackages(packages);
4796
4797 packages[this.uci_package] = true;
4798
4799 for (var p in packages)
4800 packages[p] = ['uci', 'get', { config: p }];
4801
4802 var load_cb = this._load_cb || (this._load_cb = $.proxy(function(responses) {
4803 for (var p in responses)
4804 {
4805 if (responses[p][0] != 0 || !responses[p][1] || !responses[p][1].values)
4806 continue;
4807
4808 this.uci.values[p] = responses[p][1].values;
4809 }
4810
4811 var deferreds = [ _luci2.deferrable(this.options.prepare()) ];
4812
4813 for (var i = 0; i < this.sections.length; i++)
4814 {
4815 for (var f in this.sections[i].fields)
4816 {
4817 if (typeof(this.sections[i].fields[f].load) != 'function')
4818 continue;
4819
4820 var s = this.sections[i].sections();
4821 for (var j = 0; j < s.length; j++)
4822 {
4823 var rv = this.sections[i].fields[f].load(s[j]['.name']);
4824 if (_luci2.deferred(rv))
4825 deferreds.push(rv);
4826 }
4827 }
4828 }
4829
4830 return $.when.apply($, deferreds);
4831 }, this));
4832
4833 return _luci2.rpc.call(packages).then(load_cb);
4834 },
4835
4836 render: function()
4837 {
4838 var map = $('<div />').addClass('cbi-map');
4839
4840 if (typeof(this.options.caption) == 'string')
4841 $('<h2 />').text(this.options.caption).appendTo(map);
4842
4843 if (typeof(this.options.description) == 'string')
4844 $('<div />').addClass('cbi-map-descr').text(this.options.description).appendTo(map);
4845
4846 var sections = $('<div />').appendTo(map);
4847
4848 for (var i = 0; i < this.sections.length; i++)
4849 {
4850 var s = this.sections[i].render();
4851
4852 if (this.options.readonly || this.sections[i].options.readonly)
4853 s.find('input, select, button, img.cbi-button').attr('disabled', true);
4854
4855 s.appendTo(sections);
4856
4857 if (this.sections[i].options.active)
4858 this.active_panel = i;
4859 }
4860
4861 if (this.options.collabsible)
4862 {
4863 var a = sections.accordion({
4864 header: '> fieldset > div.cbi-section-head',
4865 heightStyle: 'content',
4866 active: this.active_panel
4867 });
4868
4869 a.on('accordionbeforeactivate', { self: this }, function(ev, ui) {
4870 var h = ui.oldHeader;
4871 var s = ev.data.self.sections[parseInt(h.attr('cbi-section-num'))];
4872 var i = h.attr('cbi-section-id');
4873
4874 h.children('.cbi-section-teaser').empty().append(s.teaser(i));
4875
4876 for (var i = 0; i < ev.data.self.sections.length; i++)
4877 ev.data.self.sections[i].validate();
4878 });
4879
4880 a.on('accordionactivate', { self: this }, function(ev, ui) {
4881 ev.data.self.active_panel = parseInt(ui.newPanel.attr('index'));
4882 });
4883 }
4884
4885 if (this.options.pageaction !== false)
4886 {
4887 var a = $('<div />')
4888 .addClass('cbi-page-actions')
4889 .appendTo(map);
4890
4891 $('<input />')
4892 .addClass('cbi-button').addClass('cbi-button-apply')
4893 .attr('type', 'button')
4894 .val(_luci2.tr('Save & Apply'))
4895 .appendTo(a);
4896
4897 $('<input />')
4898 .addClass('cbi-button').addClass('cbi-button-save')
4899 .attr('type', 'button')
4900 .val(_luci2.tr('Save'))
4901 .click({ self: this }, function(ev) { ev.data.self.send(); })
4902 .appendTo(a);
4903
4904 $('<input />')
4905 .addClass('cbi-button').addClass('cbi-button-reset')
4906 .attr('type', 'button')
4907 .val(_luci2.tr('Reset'))
4908 .click({ self: this }, function(ev) { ev.data.self.insertInto(ev.data.self.target); })
4909 .appendTo(a);
4910
4911 a = null;
4912 }
4913
4914 var top = $('<form />').append(map);
4915
4916 map = null;
4917
4918 return top;
4919 },
4920
4921 finish: function()
4922 {
4923 for (var i = 0; i < this.sections.length; i++)
4924 this.sections[i].finish();
4925
4926 this.validate();
4927 },
4928
4929 redraw: function()
4930 {
4931 this.target.hide().empty().append(this.render());
4932 this.finish();
4933 this.target.show();
4934 },
4935
4936 section: function(widget, uci_type, options)
4937 {
4938 var w = widget ? new widget(uci_type, options) : null;
4939
4940 if (!(w instanceof AbstractSection))
4941 throw 'Widget must be an instance of AbstractSection';
4942
4943 w.map = this;
4944 w.index = this.sections.length;
4945
4946 this.sections.push(w);
4947 return w;
4948 },
4949
4950 formvalue: function()
4951 {
4952 var rv = { };
4953
4954 for (var i = 0; i < this.sections.length; i++)
4955 {
4956 var sids = this.sections[i].formvalue();
4957 for (var sid in sids)
4958 {
4959 var s = rv[sid] || (rv[sid] = { });
4960 $.extend(s, sids[sid]);
4961 }
4962 }
4963
4964 return rv;
4965 },
4966
4967 add: function(conf, type, name)
4968 {
4969 var c = this.uci.creates;
4970 var s = '.new.%d'.format(this.uci.newid++);
4971
4972 if (!c[conf])
4973 c[conf] = { };
4974
4975 c[conf][s] = {
4976 '.type': type,
4977 '.name': s,
4978 '.create': name,
4979 '.anonymous': !name
4980 };
4981
4982 return s;
4983 },
4984
4985 remove: function(conf, sid)
4986 {
4987 var n = this.uci.creates;
4988 var c = this.uci.changes;
4989 var d = this.uci.deletes;
4990
4991 /* requested deletion of a just created section */
4992 if (sid.indexOf('.new.') == 0)
4993 {
4994 if (n[conf])
4995 delete n[conf][sid];
4996 }
4997 else
4998 {
4999 if (c[conf])
5000 delete c[conf][sid];
5001
5002 if (!d[conf])
5003 d[conf] = { };
5004
5005 d[conf][sid] = true;
5006 }
5007 },
5008
5009 ucisections: function(conf, cb)
5010 {
5011 var sa = [ ];
5012 var pkg = this.uci.values[conf];
5013 var crt = this.uci.creates[conf];
5014 var del = this.uci.deletes[conf];
5015
5016 if (!pkg)
5017 return sa;
5018
5019 for (var s in pkg)
5020 if (!del || del[s] !== true)
5021 sa.push(pkg[s]);
5022
5023 sa.sort(function(a, b) { return a['.index'] - b['.index'] });
5024
5025 if (crt)
5026 for (var s in crt)
5027 sa.push(crt[s]);
5028
5029 if (typeof(cb) == 'function')
5030 for (var i = 0; i < sa.length; i++)
5031 cb.apply(this, [ sa[i] ]);
5032
5033 return sa;
5034 },
5035
5036 get: function(conf, sid, opt)
5037 {
5038 var v = this.uci.values;
5039 var n = this.uci.creates;
5040 var c = this.uci.changes;
5041 var d = this.uci.deletes;
5042
5043 /* requested option in a just created section */
5044 if (sid.indexOf('.new.') == 0)
5045 {
5046 if (!n[conf])
5047 return undefined;
5048
5049 if (typeof(opt) == 'undefined')
5050 return (n[conf][sid] || { });
5051
5052 return n[conf][sid][opt];
5053 }
5054
5055 /* requested an option value */
5056 if (typeof(opt) != 'undefined')
5057 {
5058 /* check whether option was deleted */
5059 if (d[conf] && d[conf][sid])
5060 {
5061 if (d[conf][sid] === true)
5062 return undefined;
5063
5064 for (var i = 0; i < d[conf][sid].length; i++)
5065 if (d[conf][sid][i] == opt)
5066 return undefined;
5067 }
5068
5069 /* check whether option was changed */
5070 if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined')
5071 return c[conf][sid][opt];
5072
5073 /* return base value */
5074 if (v[conf] && v[conf][sid])
5075 return v[conf][sid][opt];
5076
5077 return undefined;
5078 }
5079
5080 /* requested an entire section */
5081 if (v[conf])
5082 return (v[conf][sid] || { });
5083
5084 return undefined;
5085 },
5086
5087 set: function(conf, sid, opt, val)
5088 {
5089 var n = this.uci.creates;
5090 var c = this.uci.changes;
5091 var d = this.uci.deletes;
5092
5093 if (sid.indexOf('.new.') == 0)
5094 {
5095 if (n[conf] && n[conf][sid])
5096 {
5097 if (typeof(val) != 'undefined')
5098 n[conf][sid][opt] = val;
5099 else
5100 delete n[conf][sid][opt];
5101 }
5102 }
5103 else if (typeof(val) != 'undefined')
5104 {
5105 if (!c[conf])
5106 c[conf] = { };
5107
5108 if (!c[conf][sid])
5109 c[conf][sid] = { };
5110
5111 c[conf][sid][opt] = val;
5112 }
5113 else
5114 {
5115 if (!d[conf])
5116 d[conf] = { };
5117
5118 if (!d[conf][sid])
5119 d[conf][sid] = [ ];
5120
5121 d[conf][sid].push(opt);
5122 }
5123 },
5124
5125 validate: function()
5126 {
5127 var rv = true;
5128
5129 for (var i = 0; i < this.sections.length; i++)
5130 if (!this.sections[i].validate())
5131 rv = false;
5132
5133 return rv;
5134 },
5135
5136 save: function()
5137 {
5138 if (this.options.readonly)
5139 return _luci2.deferrable();
5140
5141 var deferreds = [ _luci2.deferrable(this.options.save()) ];
5142
5143 for (var i = 0; i < this.sections.length; i++)
5144 {
5145 if (this.sections[i].options.readonly)
5146 continue;
5147
5148 for (var f in this.sections[i].fields)
5149 {
5150 if (typeof(this.sections[i].fields[f].save) != 'function')
5151 continue;
5152
5153 var s = this.sections[i].sections();
5154 for (var j = 0; j < s.length; j++)
5155 {
5156 var rv = this.sections[i].fields[f].save(s[j]['.name']);
5157 if (_luci2.deferred(rv))
5158 deferreds.push(rv);
5159 }
5160 }
5161 }
5162
5163 return $.when.apply($, deferreds);
5164 },
5165
5166 send: function()
5167 {
5168 if (!this.validate())
5169 return _luci2.deferrable();
5170
5171 var send_cb = this._send_cb || (this._send_cb = $.proxy(function() {
5172 var requests = [ ];
5173
5174 if (this.uci.creates)
5175 for (var c in this.uci.creates)
5176 for (var s in this.uci.creates[c])
5177 {
5178 var r = {
5179 config: c,
5180 values: { }
5181 };
5182
5183 for (var k in this.uci.creates[c][s])
5184 {
5185 if (k == '.type')
5186 r.type = this.uci.creates[i][k];
5187 else if (k == '.create')
5188 r.name = this.uci.creates[i][k];
5189 else if (k.charAt(0) != '.')
5190 r.values[k] = this.uci.creates[i][k];
5191 }
5192 requests.push(['uci', 'add', r]);
5193 }
5194
5195 if (this.uci.changes)
5196 for (var c in this.uci.changes)
5197 for (var s in this.uci.changes[c])
5198 requests.push(['uci', 'set', {
5199 config: c,
5200 section: s,
5201 values: this.uci.changes[c][s]
5202 }]);
5203
5204 if (this.uci.deletes)
5205 for (var c in this.uci.deletes)
5206 for (var s in this.uci.deletes[c])
5207 {
5208 var o = this.uci.deletes[c][s];
5209 requests.push(['uci', 'delete', {
5210 config: c,
5211 section: s,
5212 options: (o === true) ? undefined : o
5213 }]);
5214 }
5215
5216 return _luci2.rpc.call(requests);
5217 }, this));
5218
5219 var self = this;
5220
5221 _luci2.ui.loading(true);
5222
5223 return this.save().then(send_cb).then(function() {
5224 return self.load();
5225 }).then(function() {
5226 self.redraw();
5227 self = null;
5228
5229 _luci2.ui.loading(false);
5230 });
5231 },
5232
5233 dialog: function(id)
5234 {
5235 var d = $('<div />');
5236 var p = $('<p />');
5237
5238 $('<img />')
5239 .attr('src', _luci2.globals.resource + '/icons/loading.gif')
5240 .css('vertical-align', 'middle')
5241 .css('padding-right', '10px')
5242 .appendTo(p);
5243
5244 p.append(_luci2.tr('Loading data...'));
5245
5246 p.appendTo(d);
5247 d.appendTo(id);
5248
5249 return d.dialog({
5250 modal: true,
5251 draggable: false,
5252 resizable: false,
5253 height: 90,
5254 open: function() {
5255 $(this).parent().children('.ui-dialog-titlebar').hide();
5256 }
5257 });
5258 },
5259
5260 insertInto: function(id)
5261 {
5262 var self = this;
5263 self.target = $(id);
5264
5265 _luci2.ui.loading(true);
5266 self.target.hide();
5267
5268 return self.load().then(function() {
5269 self.target.empty().append(self.render());
5270 self.finish();
5271 self.target.show();
5272 self = null;
5273 _luci2.ui.loading(false);
5274 });
5275 }
5276 });
5277 };