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