luci2: rework datatype validators to use new global parseIPv4(), parseIPv6() and...
[project/luci2/ui.git] / luci2 / htdocs / luci2 / luci2.js
index 6b07f8c..e47f5bd 100644 (file)
@@ -485,6 +485,152 @@ function LuCI2()
                return n;
        };
 
+       this.toColor = function(str)
+       {
+               if (typeof(str) != 'string' || str.length == 0)
+                       return '#CCCCCC';
+
+               if (str == 'wan')
+                       return '#F09090';
+               else if (str == 'lan')
+                       return '#90F090';
+
+               var i = 0, hash = 0;
+
+               while (i < str.length)
+                       hash = str.charCodeAt(i++) + ((hash << 5) - hash);
+
+               var r = (hash & 0xFF) % 128;
+               var g = ((hash >> 8) & 0xFF) % 128;
+
+               var min = 0;
+               var max = 128;
+
+               if ((r + g) < 128)
+                       min = 128 - r - g;
+               else
+                       max = 255 - r - g;
+
+               var b = min + (((hash >> 16) & 0xFF) % (max - min));
+
+               return '#%02X%02X%02X'.format(0xFF - r, 0xFF - g, 0xFF - b);
+       };
+
+       this.parseIPv4 = function(str)
+       {
+               if ((typeof(str) != 'string' && !(str instanceof String)) ||
+                   !str.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
+                       return undefined;
+
+               var num = [ ];
+               var parts = str.split(/\./);
+
+               for (var i = 0; i < parts.length; i++)
+               {
+                       var n = parseInt(parts[i], 10);
+                       if (isNaN(n) || n > 255)
+                               return undefined;
+
+                       num.push(n);
+               }
+
+               return num;
+       };
+
+       this.parseIPv6 = function(str)
+       {
+               if ((typeof(str) != 'string' && !(str instanceof String)) ||
+                   !str.match(/^[a-fA-F0-9:]+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/))
+                       return undefined;
+
+               var parts = str.split(/::/);
+               if (parts.length == 0 || parts.length > 2)
+                       return undefined;
+
+               var lnum = [ ];
+               if (parts[0].length > 0)
+               {
+                       var left = parts[0].split(/:/);
+                       for (var i = 0; i < left.length; i++)
+                       {
+                               var n = parseInt(left[i], 16);
+                               if (isNaN(n))
+                                       return undefined;
+
+                               lnum.push((n / 256) >> 0);
+                               lnum.push(n % 256);
+                       }
+               }
+
+               var rnum = [ ];
+               if (parts.length > 1 && parts[1].length > 0)
+               {
+                       var right = parts[1].split(/:/);
+
+                       for (var i = 0; i < right.length; i++)
+                       {
+                               if (right[i].indexOf('.') > 0)
+                               {
+                                       var addr = L.parseIPv4(right[i]);
+                                       if (!addr)
+                                               return undefined;
+
+                                       rnum.push.apply(rnum, addr);
+                                       continue;
+                               }
+
+                               var n = parseInt(right[i], 16);
+                               if (isNaN(n))
+                                       return undefined;
+
+                               rnum.push((n / 256) >> 0);
+                               rnum.push(n % 256);
+                       }
+               }
+
+               if (rnum.length > 0 && (lnum.length + rnum.length) > 15)
+                       return undefined;
+
+               var num = [ ];
+
+               num.push.apply(num, lnum);
+
+               for (var i = 0; i < (16 - lnum.length - rnum.length); i++)
+                       num.push(0);
+
+               num.push.apply(num, rnum);
+
+               if (num.length > 16)
+                       return undefined;
+
+               return num;
+       };
+
+       this.isNetmask = function(addr)
+       {
+               if (!$.isArray(addr))
+                       return false;
+
+               var c;
+
+               for (c = 0; (c < addr.length) && (addr[c] == 255); c++);
+
+               if (c == addr.length)
+                       return true;
+
+               if ((addr[c] == 254) || (addr[c] == 252) || (addr[c] == 248) ||
+                       (addr[c] == 240) || (addr[c] == 224) || (addr[c] == 192) ||
+                       (addr[c] == 128) || (addr[c] == 0))
+               {
+                       for (c++; (c < addr.length) && (addr[c] == 0); c++);
+
+                       if (c == addr.length)
+                               return true;
+               }
+
+               return false;
+       };
+
        this.globals = {
                timeout:  15000,
                resource: '/luci2',
@@ -674,7 +820,7 @@ function LuCI2()
                init: function()
                {
                        this.state = {
-                               newid  0,
+                               newidx:  0,
                                values:  { },
                                creates: { },
                                changes: { },
@@ -715,6 +861,19 @@ function LuCI2()
                        params: [ 'config', 'section', 'options' ]
                }),
 
+               _newid: function(conf)
+               {
+                       var v = this.state.values;
+                       var n = this.state.creates;
+                       var sid;
+
+                       do {
+                               sid = "new%06x".format(Math.random() * 0xFFFFFF);
+                       } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
+
+                       return sid;
+               },
+
                load: function(packages)
                {
                        var self = this;
@@ -727,7 +886,7 @@ function LuCI2()
                        L.rpc.batch();
 
                        for (var i = 0; i < packages.length; i++)
-                               if (!seen[packages[i]])
+                               if (!seen[packages[i]] && !self.state.values[packages[i]])
                                {
                                        pkgs.push(packages[i]);
                                        seen[packages[i]] = true;
@@ -758,21 +917,21 @@ function LuCI2()
 
                add: function(conf, type, name)
                {
-                       var c = this.state.creates;
-                       var s = '.new.%d'.format(this.state.newid++);
+                       var n = this.state.creates;
+                       var sid = this._newid(conf);
 
-                       if (!c[conf])
-                               c[conf] = { };
+                       if (!n[conf])
+                               n[conf] = { };
 
-                       c[conf][s] = {
+                       n[conf][sid] = {
                                '.type':      type,
-                               '.name':      s,
+                               '.name':      sid,
                                '.create':    name,
                                '.anonymous': !name,
-                               '.index':     1000 + this.state.newid
+                               '.index':     1000 + this.state.newidx++
                        };
 
-                       return s;
+                       return sid;
                },
 
                remove: function(conf, sid)
@@ -782,10 +941,9 @@ function LuCI2()
                        var d = this.state.deletes;
 
                        /* requested deletion of a just created section */
-                       if (sid.indexOf('.new.') == 0)
+                       if (n[conf] && n[conf][sid])
                        {
-                               if (n[conf])
-                                       delete n[conf][sid];
+                               delete n[conf][sid];
                        }
                        else
                        {
@@ -845,7 +1003,7 @@ function LuCI2()
                                return undefined;
 
                        /* requested option in a just created section */
-                       if (sid.indexOf('.new.') == 0)
+                       if (n[conf] && n[conf][sid])
                        {
                                if (!n[conf])
                                        return undefined;
@@ -890,6 +1048,7 @@ function LuCI2()
 
                set: function(conf, sid, opt, val)
                {
+                       var v = this.state.values;
                        var n = this.state.creates;
                        var c = this.state.changes;
                        var d = this.state.deletes;
@@ -899,15 +1058,12 @@ function LuCI2()
                            opt.charAt(0) == '.')
                                return;
 
-                       if (sid.indexOf('.new.') == 0)
+                       if (n[conf] && n[conf][sid])
                        {
-                               if (n[conf] && n[conf][sid])
-                               {
-                                       if (typeof(val) != 'undefined')
-                                               n[conf][sid][opt] = val;
-                                       else
-                                               delete n[conf][sid][opt];
-                               }
+                               if (typeof(val) != 'undefined')
+                                       n[conf][sid][opt] = val;
+                               else
+                                       delete n[conf][sid][opt];
                        }
                        else if (typeof(val) != 'undefined')
                        {
@@ -915,6 +1071,10 @@ function LuCI2()
                                if (d[conf] && d[conf][sid] === true)
                                        return;
 
+                               /* only set in existing sections */
+                               if (!v[conf] || !v[conf][sid])
+                                       return;
+
                                if (!c[conf])
                                        c[conf] = { };
 
@@ -929,6 +1089,10 @@ function LuCI2()
                        }
                        else
                        {
+                               /* only delete in existing sections */
+                               if (!v[conf] || !v[conf][sid])
+                                       return;
+
                                if (!d[conf])
                                        d[conf] = { };
 
@@ -945,6 +1109,35 @@ function LuCI2()
                        return this.set(conf, sid, opt, undefined);
                },
 
+               get_first: function(conf, type, opt)
+               {
+                       var sid = undefined;
+
+                       L.uci.sections(conf, type, function(s) {
+                               if (typeof(sid) != 'string')
+                                       sid = s['.name'];
+                       });
+
+                       return this.get(conf, sid, opt);
+               },
+
+               set_first: function(conf, type, opt, val)
+               {
+                       var sid = undefined;
+
+                       L.uci.sections(conf, type, function(s) {
+                               if (typeof(sid) != 'string')
+                                       sid = s['.name'];
+                       });
+
+                       return this.set(conf, sid, opt, val);
+               },
+
+               unset_first: function(conf, type, opt)
+               {
+                       return this.set_first(conf, type, opt, undefined);
+               },
+
                _reload: function()
                {
                        var pkgs = [ ];
@@ -976,7 +1169,7 @@ function LuCI2()
                        {
                                var o = [ ];
 
-                               if (n && n[c])
+                               if (n[c])
                                        for (var s in n[c])
                                                o.push(n[c][s]);
 
@@ -1024,46 +1217,64 @@ function LuCI2()
                {
                        L.rpc.batch();
 
+                       var v = this.state.values;
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
+
                        var self = this;
                        var snew = [ ];
+                       var pkgs = { };
 
-                       if (self.state.creates)
-                               for (var c in self.state.creates)
-                                       for (var s in self.state.creates[c])
+                       if (n)
+                               for (var conf in n)
+                               {
+                                       for (var sid in n[conf])
                                        {
                                                var r = {
-                                                       config: c,
+                                                       config: conf,
                                                        values: { }
                                                };
 
-                                               for (var k in self.state.creates[c][s])
+                                               for (var k in n[conf][sid])
                                                {
                                                        if (k == '.type')
-                                                               r.type = self.state.creates[c][s][k];
+                                                               r.type = n[conf][sid][k];
                                                        else if (k == '.create')
-                                                               r.name = self.state.creates[c][s][k];
+                                                               r.name = n[conf][sid][k];
                                                        else if (k.charAt(0) != '.')
-                                                               r.values[k] = self.state.creates[c][s][k];
+                                                               r.values[k] = n[conf][sid][k];
                                                }
 
-                                               snew.push(self.state.creates[c][s]);
+                                               snew.push(n[conf][sid]);
 
                                                self._add(r.config, r.type, r.name, r.values);
                                        }
 
-                       if (self.state.changes)
-                               for (var c in self.state.changes)
-                                       for (var s in self.state.changes[c])
-                                               self._set(c, s, self.state.changes[c][s]);
+                                       pkgs[conf] = true;
+                               }
+
+                       if (c)
+                               for (var conf in c)
+                               {
+                                       for (var sid in c[conf])
+                                               self._set(conf, sid, c[conf][sid]);
+
+                                       pkgs[conf] = true;
+                               }
 
-                       if (self.state.deletes)
-                               for (var c in self.state.deletes)
-                                       for (var s in self.state.deletes[c])
+                       if (d)
+                               for (var conf in d)
+                               {
+                                       for (var sid in d[conf])
                                        {
-                                               var o = self.state.deletes[c][s];
-                                               self._delete(c, s, (o === true) ? undefined : o);
+                                               var o = d[conf][sid];
+                                               self._delete(conf, sid, (o === true) ? undefined : o);
                                        }
 
+                                       pkgs[conf] = true;
+                               }
+
                        return L.rpc.flush().then(function(responses) {
                                /*
                                 array "snew" holds references to the created uci sections,
@@ -1073,6 +1284,12 @@ function LuCI2()
                                        snew[i]['.name'] = responses[i];
 
                                return self._reorder();
+                       }).then(function() {
+                               pkgs = L.toArray(pkgs);
+
+                               self.unload(pkgs);
+
+                               return self.load(pkgs);
                        });
                },
 
@@ -1441,7 +1658,9 @@ function LuCI2()
                _fetch_protocols: function()
                {
                        var self = L.NetworkModel;
-                       var deferreds = [ ];
+                       var deferreds = [
+                               self._fetch_protocol('none')
+                       ];
 
                        for (var proto in self._cache.protolist)
                                deferreds.push(self._fetch_protocol(proto));
@@ -2518,6 +2737,30 @@ function LuCI2()
                        return dev.getTrafficHistory();
                },
 
+               renderBadge: function()
+               {
+                       var badge = $('<span />')
+                               .addClass('badge')
+                               .text('%s: '.format(this.name()));
+
+                       var dev = this.getDevice();
+                       var subdevs = this.getSubdevices();
+
+                       if (subdevs.length)
+                               for (var j = 0; j < subdevs.length; j++)
+                                       badge.append($('<img />')
+                                               .attr('src', subdevs[j].icon())
+                                               .attr('title', '%s (%s)'.format(subdevs[j].description(), subdevs[j].name() || '?')));
+                       else if (dev)
+                               badge.append($('<img />')
+                                       .attr('src', dev.icon())
+                                       .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?')));
+                       else
+                               badge.append($('<em />').text(L.tr('(No devices attached)')));
+
+                       return badge;
+               },
+
                setDevices: function(devs)
                {
                        var dev = this.getPhysdev();
@@ -3686,6 +3929,33 @@ function LuCI2()
 
                appendTo: function(id) {
                        return $(id).append(this.render());
+               },
+
+               on: function(evname, evfunc)
+               {
+                       var evnames = L.toArray(evname);
+
+                       if (!this.events)
+                               this.events = { };
+
+                       for (var i = 0; i < evnames.length; i++)
+                               this.events[evnames[i]] = evfunc;
+
+                       return this;
+               },
+
+               trigger: function(evname, evdata)
+               {
+                       if (this.events)
+                       {
+                               var evnames = L.toArray(evname);
+
+                               for (var i = 0; i < evnames.length; i++)
+                                       if (this.events[evnames[i]])
+                                               this.events[evnames[i]].call(this, evdata);
+                       }
+
+                       return this;
                }
        });
 
@@ -4341,8 +4611,7 @@ function LuCI2()
 
                'ipaddr': function()
                {
-                       if (validation.types['ip4addr'].apply(this) ||
-                               validation.types['ip6addr'].apply(this))
+                       if (L.parseIPv4(this) || L.parseIPv6(this))
                                return true;
 
                        validation.i18n('Must be a valid IP address');
@@ -4351,17 +4620,8 @@ function LuCI2()
 
                'ip4addr': function()
                {
-                       if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/))
-                       {
-                               if ((RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
-                                   (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
-                                   (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
-                                   (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
-                                   ((RegExp.$6.indexOf('.') < 0)
-                                     ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32))
-                                     : (validation.types['ip4addr'].apply(RegExp.$6))))
-                                       return true;
-                       }
+                       if (L.parseIPv4(this))
+                               return true;
 
                        validation.i18n('Must be a valid IPv4 address');
                        return false;
@@ -4369,62 +4629,74 @@ function LuCI2()
 
                'ip6addr': function()
                {
-                       if (this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/))
-                       {
-                               if (!RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)))
-                               {
-                                       var addr = RegExp.$1;
+                       if (L.parseIPv6(this))
+                               return true;
 
-                                       if (addr == '::')
-                                       {
-                                               return true;
-                                       }
+                       validation.i18n('Must be a valid IPv6 address');
+                       return false;
+               },
 
-                                       if (addr.indexOf('.') > 0)
-                                       {
-                                               var off = addr.lastIndexOf(':');
+               'netmask4': function()
+               {
+                       if (L.isNetmask(L.parseIPv4(this)))
+                               return true;
 
-                                               if (!(off && validation.types['ip4addr'].apply(addr.substr(off+1))))
-                                               {
-                                                       validation.i18n('Must be a valid IPv6 address');
-                                                       return false;
-                                               }
+                       validation.i18n('Must be a valid IPv4 netmask');
+                       return false;
+               },
 
-                                               addr = addr.substr(0, off) + ':0:0';
-                                       }
+               'netmask6': function()
+               {
+                       if (L.isNetmask(L.parseIPv6(this)))
+                               return true;
 
-                                       if (addr.indexOf('::') >= 0)
-                                       {
-                                               var colons = 0;
-                                               var fill = '0';
+                       validation.i18n('Must be a valid IPv6 netmask6');
+                       return false;
+               },
 
-                                               for (var i = 1; i < (addr.length-1); i++)
-                                                       if (addr.charAt(i) == ':')
-                                                               colons++;
+               'cidr4': function()
+               {
+                       if (this.match(/^([0-9.]+)\/(\d{1,2})$/))
+                               if (RegExp.$2 <= 32 && L.parseIPv4(RegExp.$1))
+                                       return true;
 
-                                               if (colons > 7)
-                                               {
-                                                       validation.i18n('Must be a valid IPv6 address');
-                                                       return false;
-                                               }
+                       validation.i18n('Must be a valid IPv4 prefix');
+                       return false;
+               },
 
-                                               for (var i = 0; i < (7 - colons); i++)
-                                                       fill += ':0';
+               'cidr6': function()
+               {
+                       if (this.match(/^([a-fA-F0-9:.]+)\/(\d{1,3})$/))
+                               if (RegExp.$2 <= 128 && L.parseIPv6(RegExp.$1))
+                                       return true;
 
-                                               if (addr.match(/^(.*?)::(.*?)$/))
-                                                       addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
-                                                                  (RegExp.$2 ? ':' + RegExp.$2 : '');
-                                       }
+                       validation.i18n('Must be a valid IPv6 prefix');
+                       return false;
+               },
 
-                                       if (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null)
-                                               return true;
+               'ipmask4': function()
+               {
+                       if (this.match(/^([0-9.]+)\/([0-9.]+)$/))
+                       {
+                               var addr = RegExp.$1, mask = RegExp.$2;
+                               if (L.parseIPv4(addr) && L.isNetmask(L.parseIPv4(mask)))
+                                       return true;
+                       }
 
-                                       validation.i18n('Must be a valid IPv6 address');
-                                       return false;
-                               }
+                       validation.i18n('Must be a valid IPv4 address/netmask pair');
+                       return false;
+               },
+
+               'ipmask6': function()
+               {
+                       if (this.match(/^([a-fA-F0-9:.]+)\/([a-fA-F0-9:.]+)$/))
+                       {
+                               var addr = RegExp.$1, mask = RegExp.$2;
+                               if (L.parseIPv6(addr) && L.isNetmask(L.parseIPv6(mask)))
+                                       return true;
                        }
 
-                       validation.i18n('Must be a valid IPv6 address');
+                       validation.i18n('Must be a valid IPv6 address/netmask pair');
                        return false;
                },
 
@@ -4704,7 +4976,6 @@ function LuCI2()
                        this.instance = { };
                        this.dependencies = [ ];
                        this.rdependency = { };
-                       this.events = { };
 
                        this.options = L.defaults(options, {
                                placeholder: '',
@@ -4925,8 +5196,9 @@ function LuCI2()
                                opt:    this.options.optional
                        };
 
-                       for (var evname in this.events)
-                               elem.on(evname, evdata, this.events[evname]);
+                       if (this.events)
+                               for (var evname in this.events)
+                                       elem.on(evname, evdata, this.events[evname]);
 
                        if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
                                return elem;
@@ -5102,12 +5374,6 @@ function LuCI2()
                        }
 
                        return false;
-               },
-
-               on: function(evname, evfunc)
-               {
-                       this.events[evname] = evfunc;
-                       return this;
                }
        });
 
@@ -5770,30 +6036,16 @@ function LuCI2()
                        for (var i = 0; i < interfaces.length; i++)
                        {
                                var iface = interfaces[i];
-                               var badge = $('<span />')
-                                       .addClass('badge')
-                                       .text('%s: '.format(iface.name()));
-
-                               var dev = iface.getDevice();
-                               var subdevs = iface.getSubdevices();
-
-                               if (subdevs.length)
-                                       for (var j = 0; j < subdevs.length; j++)
-                                               badge.append(this._device_icon(subdevs[j]));
-                               else if (dev)
-                                       badge.append(this._device_icon(dev));
-                               else
-                                       badge.append($('<em />').text(L.tr('(No devices attached)')));
 
                                $('<li />')
                                        .append($('<label />')
                                                .addClass(itype + ' inline')
-                                               .append($('<input />')
+                                               .append(this.validator(sid, $('<input />')
                                                        .attr('name', itype + id)
                                                        .attr('type', itype)
                                                        .attr('value', iface.name())
-                                                       .prop('checked', !!check[iface.name()]))
-                                               .append(badge))
+                                                       .prop('checked', !!check[iface.name()]), true))
+                                               .append(iface.renderBadge()))
                                        .appendTo(ul);
                        }
 
@@ -6237,12 +6489,12 @@ function LuCI2()
 
                add: function(name)
                {
-                       this.map.add(this.map.uci_package, this.uci_type, name);
+                       return this.map.add(this.map.uci_package, this.uci_type, name);
                },
 
                remove: function(sid)
                {
-                       this.map.remove(this.map.uci_package, sid);
+                       return this.map.remove(this.map.uci_package, sid);
                },
 
                _ev_add: function(ev)
@@ -6261,7 +6513,13 @@ function LuCI2()
 
                        self.active_panel = -1;
                        self.map.save();
-                       self.add(name);
+
+                       ev.data.sid  = self.add(name);
+                       ev.data.type = self.uci_type;
+                       ev.data.name = name;
+
+                       self.trigger('add', ev);
+
                        self.map.redraw();
 
                        L.ui.restoreScrollTop();
@@ -6274,6 +6532,8 @@ function LuCI2()
 
                        L.ui.saveScrollTop();
 
+                       self.trigger('remove', ev);
+
                        self.map.save();
                        self.remove(sid);
                        self.map.redraw();
@@ -6928,6 +7188,30 @@ function LuCI2()
                        self.active_tab = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
                },
 
+               _ev_apply: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.trigger('apply', ev);
+               },
+
+               _ev_save: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.send().then(function() {
+                               self.trigger('save', ev);
+                       });
+               },
+
+               _ev_reset: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.trigger('reset', ev);
+                       self.reset();
+               },
+
                _render_tab_head: function(tab_index)
                {
                        var section = this.sections[tab_index];
@@ -7002,16 +7286,20 @@ function LuCI2()
 
                _render_footer: function()
                {
+                       var evdata = {
+                               self: this
+                       };
+
                        return $('<div />')
                                .addClass('panel panel-default panel-body text-right')
                                .append($('<div />')
                                        .addClass('btn-group')
                                        .append(L.ui.button(L.tr('Save & Apply'), 'primary')
-                                               .click({ self: this }, function(ev) {  }))
+                                               .click(evdata, this._ev_apply))
                                        .append(L.ui.button(L.tr('Save'), 'default')
-                                               .click({ self: this }, function(ev) { ev.data.self.send(); }))
+                                               .click(evdata, this._ev_save))
                                        .append(L.ui.button(L.tr('Reset'), 'default')
-                                               .click({ self: this }, function(ev) { ev.data.self.insertInto(ev.data.self.target); })));
+                                               .click(evdata, this._ev_reset)));
                },
 
                render: function()
@@ -7172,6 +7460,27 @@ function LuCI2()
                        });
                },
 
+               revert: function()
+               {
+                       var packages = { };
+
+                       for (var i = 0; i < this.sections.length; i++)
+                               this.sections[i].ucipackages(packages);
+
+                       packages[this.uci_package] = true;
+
+                       L.uci.unload(L.toArray(packages));
+               },
+
+               reset: function()
+               {
+                       var self = this;
+
+                       self.revert();
+
+                       return self.insertInto(self.target);
+               },
+
                insertInto: function(id)
                {
                        var self = this;
@@ -7191,16 +7500,46 @@ function LuCI2()
        });
 
        this.cbi.Modal = this.cbi.Map.extend({
+               _ev_apply: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.trigger('apply', ev);
+               },
+
+               _ev_save: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.send().then(function() {
+                               self.trigger('save', ev);
+                               self.close();
+                       });
+               },
+
+               _ev_reset: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.trigger('close', ev);
+                       self.revert();
+                       self.close();
+               },
+
                _render_footer: function()
                {
+                       var evdata = {
+                               self: this
+                       };
+
                        return $('<div />')
                                .addClass('btn-group')
                                .append(L.ui.button(L.tr('Save & Apply'), 'primary')
-                                       .click({ self: this }, function(ev) {  }))
+                                       .click(evdata, this._ev_apply))
                                .append(L.ui.button(L.tr('Save'), 'default')
-                                       .click({ self: this }, function(ev) { ev.data.self.send(); }))
+                                       .click(evdata, this._ev_save))
                                .append(L.ui.button(L.tr('Cancel'), 'default')
-                                       .click({ self: this }, function(ev) { L.ui.dialog(false); }));
+                                       .click(evdata, this._ev_reset));
                },
 
                render: function()
@@ -7238,6 +7577,11 @@ function LuCI2()
 
                                L.ui.loading(false);
                        });
+               },
+
+               close: function()
+               {
+                       L.ui.dialog(false);
                }
        });
 };