Experimental js schema validation for UCI objects.

This commit is contained in:
Martin Schröder 2015-04-30 13:35:55 +02:00 committed by Martin Schröder
parent 324abb5030
commit 3b5d7b368e
17 changed files with 392 additions and 103 deletions

View file

@ -23,6 +23,7 @@
</div>-->
<div style="margin-bottom: 40px"></div>
<script src="/lib/js/async.js"></script>
<script src="/lib/js/js-schema.min.js"></script>
<script src="/lib/js/require.js"></script>
<script src="/lib/js/jquery.min.js"></script>
<script src="/lib/js/angular.min.js"></script>

View file

@ -147,31 +147,62 @@ angular.module("luci", [
});
})
//window.app = angular.module("luci");
/*
angular.module("luci").controller("BodyCtrl", function ($scope, $templateCache, $localStorage, $state, $session, $location, $window, $rootScope, $config, $http) {
$scope.menuClass = function(page) {
var current = $location.path().substring(1);
return page === current ? "active" : "";
};
$scope.modeList = [{
id: 0,
label: "Basic Mode"
}];
angular.module("luci")
.factory("$hosts", function($rpc, $uci){
var hosts = {};
var host_schema = schema({
hostname: /[a-zA-Z0-9]*/,
macaddr: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/
});
return {
insert: function(obj){
var deferred = $.Deferred();
deferred.resolve(obj);
return deferred.promise();
},
select: function(rules){
var mac = rules.macaddr;
var deferred = $.Deferred();
if(mac in hosts) deferred.resolve(hosts[mac]);
else {
async.series([
function(next){
$uci.show("hosts").done(function(result){
Object.keys(result).map(function(k){
var host = result[k];
if(!host_schema(host)) {
console.log("ERROR processing host "+k+": "+JSON.stringify(host_schema.errors(host)));
//return;
}
hosts[host.macaddr] = host;
});
next();
}).fail(function(){ next(); });
},
function(next){
$rpc.router.clients().done(function(clients){
Object.keys(clients).map(function(x){
var cl = clients[x];
if(!(cl.macaddr in hosts)){
hosts[cl.macaddr] = {
hostname: cl.hostname,
macaddr: cl.macaddr
};
}
});
next();
}).fail(function(){ next(); });
}
], function(){
console.log("HOSTS: "+JSON.stringify(hosts));
if(!(mac in hosts)) deferred.reject();
else deferred.resolve(hosts[mac]);
});
}
return deferred.promise();
}
}
});
*/
/*
angular.module("luci").directive('inverted', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
ngModel.$parsers.push(function(val) { return !val; });
ngModel.$formatters.push(function(val) { return !val; });
}
};
});*/
$(document).ready(function(){

View file

@ -19,10 +19,122 @@
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
// uci module for interacting with uci tables
angular.module("luci")
.factory('$uci', function($rpc, $rootScope){
// TODO: schemas must be supplied by the router.
var schemas = {
"wifi-device": {
schema: {
"type": String,
"country": String,
"band": [ "a", "b" ],
"bandwidth": Number,
"channel": [ "auto", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ],
"scantimer": Number,
"wmm": Boolean,
"wmm_noack": Boolean,
"wmm_apsd": Boolean,
"txpower": Number,
"rateset": [ "default" ],
"frag": Number,
"rts": Number,
"dtim_period": Number,
"beacon_int": Number,
"rxchainps": Boolean,
"rxchainps_qt": Number,
"rxchainps_pps": Number,
"rifs": Boolean,
"rifs_advert": Boolean,
"maxassoc": Number,
"doth": Boolean,
"hwmode": [ "auto", "11ac" ],
"radio": [ "on", "off" ]
},
defaults: {
"type": "broadcom",
"country": "EU\/13",
"band": "b",
"bandwidth": 20,
"channel": "auto",
"scantimer": 15,
"wmm": 1,
"wmm_noack": 0,
"wmm_apsd": 0,
"txpower": 100,
"rateset": "default",
"frag": 0,
"rts": 0,
"dtim_period": 1,
"beacon_int": 100,
"rxchainps": 0,
"rxchainps_qt": 10,
"rxchainps_pps": 10,
"rifs": 0,
"rifs_advert": 0,
"maxassoc": 16,
"doth": 0,
"hwmode": "auto",
"radio": "on"
}
},
"wifi-iface": {
schema: {
"device": /^wl0|wl1$/,
"network": [ "wan", "lan" ],
"mode": [ "ap" ],
"ssid": String,
"encryption": /^none|wpa|wpa2|mixed-wpa|wep-shared|mixed-psk$/,
"cipher": [ "auto" ],
"key": String,
"gtk_rekey": Boolean,
"wps_pbc": Boolean,
"wmf_bss_enable": Boolean,
"bss_max": Number,
"instance": Number,
"up": Boolean,
"disabled": Boolean,
"macmode": [ 0, 1, 2 ],
"macfilter": Boolean,
"maclist": Array(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/)
},
defaults: {
"device": "wl0",
"network": "lan",
"mode": "ap",
"ssid": "",
"encryption": "mixed-psk",
"cipher": "auto",
"key": "",
"gtk_rekey": 0,
"wps_pbc": 1,
"wmf_bss_enable": 1,
"bss_max": 16,
"instance": 1.2,
"up": 1,
"disabled": 0,
"open": 0,
"macfilter_mode": 2,
"enabled": 1,
"macfilter": 1,
"maclist": ["00:00:00:00:00:00"],
"macmode": 0,
"ifname": "wl1"
}
},
"host": {
schema: {
"hostname": String,
"macaddr": /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/
},
defaults: {
"hostname": "",
"macaddr": "00:00:00:00:00:00"
}
}
};
function initReq(path){
var parts = path.split(".");
var req = {};
@ -32,13 +144,106 @@ angular.module("luci")
if(parts.length > 2) req.option = parts[2];
return req;
}
function stringify(obj) {
var ret = {};
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
//console.log(property+": "+(typeof obj[property])+", array: "+(obj[property] instanceof Array));
if(obj[property] instanceof Array){
ret[property] = obj[property]; // skip arrays
} else if (typeof obj[property] == "object"){
ret[property] = stringify(obj[property]);
} else {
ret[property] = String(obj[property]);
if(ret[property] === "true") ret[property] = "1";
if(ret[property] === "false") ret[property] = "0";
}
}
}
return ret;
}
// validates a uci object against scheme using it's type and replaces
// invalid values with defaults.
function fixup_values(values, insert_defaults, path){
// converts all strings that are numbers to actual numbers in object
function fixup(obj, sc) {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
if (typeof obj[property] == "object" && !(obj[property] instanceof Array)){
fixup(obj[property], (sc.schema||{})[property]);
} else {
var num = Number(obj[property]);
if(!isNaN(num)) obj[property] = num;
var type = (sc.schema||{})[property];
if(!(property in obj)) {
obj[property] = sc.defaults[property];
}
if(type) {
var errors = [];
var def = sc.defaults[property];
if(type == Boolean){
if(obj[property] == 0) obj[property] = false;
if(obj[property] == 1) obj[property] = true;
} else if(type instanceof Array && obj[property] instanceof Array){
obj[property] = obj[property].map(function(x){
var err = schema(type).errors(x);
if(err) {
errors.push(err);
return ((def instanceof Array)?def[0]:def);
}
return x;
});
} else {
var err = schema(type).errors(obj[property]);
if(err) {
obj[property] = def;
errors.push(err);
}
}
if(errors.length){
var name = (path||"")+".@"+obj[".type"]+"["+obj[".name"]+"]."+property;
console.error("UCI: Failed to validate field "+name+", resettings to: "+JSON.stringify(sc.defaults[property]+": "+JSON.stringify(errors)));
}
}
}
}
}
return obj;
}
var fixed = {};
if(".type" in values){
var sc = (schemas[values[".type"]]||{});
fixed = fixup(values, sc);
//validate(sc, values);
} else {
fixed = {};
Object.keys(values).map(function(k){
var obj = values[k];
if(!(".type" in obj)){
console.log("Object missing type! ("+k+")");
} else {
var sc = (schemas[obj[".type"]]||{});
fixed[k] = fixup(obj, sc);
//validate(sc, obj);
}
});
}
return fixed;
}
return {
show: function(path){
var deferred = $.Deferred();
var req = initReq(path);
$rpc.uci.state(req).done(function(state){
if(state && state.values) deferred.resolve(state.values);
else if(state && state.value) deferred.resolve(state.value);
var fixed = null;
try {
if(state && state.values) fixed = fixup_values(state.values);
else if(state && state.value) fixed = fixup_values(state.value);
} catch(err) { console.error(err); deferred.reject(err); };
if(fixed) deferred.resolve(fixed);
else deferred.reject();
}).fail(function(){
deferred.reject();
@ -48,7 +253,7 @@ angular.module("luci")
set: function(path, values){
var deferred = $.Deferred();
var req = initReq(path);
req.values = values;
req.values = stringify(fixup_values(values));
$rpc.uci.set(req).done(function(state){
deferred.resolve();
}).fail(function(){

File diff suppressed because one or more lines are too long

View file

@ -14,6 +14,7 @@
"widgets/luci.config",
"widgets/uci.wireless.interface",
"widgets/uci.wireless.interface.macfilter.edit",
"widgets/uci.wireless.device.edit",
"widgets/uci.firewall.nat.rule.edit",
"widgets/core.modal"
],

View file

@ -0,0 +1,25 @@
<div>
<luci-config-section>
<luci-config-lines>
<luci-config-line title="'Wifi Mode'">
<ui-select ng-model="device.mode" search-enabled="false" style="width: 200px;">
<ui-select-match placeholder="{{ 'Choose Mode' | translate}}">{{"mode."+$select.selected}}</ui-select-match>
<ui-select-choices repeat="item in allModes" refresh-delay="0"><div>{{"mode."+item}}</div></ui-select-choices>
</ui-select>
</luci-config-line>
<luci-config-line title="'Bandwidth'">
<ui-select ng-model="device.bandwidth" theme="bootstrap" search-enabled="false" style="width: 200px;">
<ui-select-match placeholder="{{ 'Choose Bandwidth' | translate}}">{{"bandwidth."+$select.selected}}</ui-select-match>
<ui-select-choices repeat="item in allBandwidths" refresh-delay="0"><div >{{"bandwidth."+item}}</div></ui-select-choices>
</ui-select>
</luci-config-line>
<luci-config-line title="'Channel'">
<ui-select ng-model="device.channel" theme="bootstrap" search-enabled="false" style="width: 200px;">
<ui-select-match placeholder="{{ 'Choose Channel' | translate}}">{{"channel."+$select.selected}}</ui-select-match>
<ui-select-choices repeat="item in allChannels" refresh-delay="0"><div >{{"channel."+item}}</div></ui-select-choices>
</ui-select>
</luci-config-line>
</luci-config-lines>
</luci-config-section>
</div>

View file

@ -0,0 +1,18 @@
$juci.module("core")
.directive("uciWirelessDeviceEdit", function($compile){
var plugin_root = $juci.module("core").plugin_root;
return {
templateUrl: plugin_root+"/widgets/uci.wireless.device.edit.html",
scope: {
device: "=ngModel"
},
controller: "WifiDeviceEditController",
replace: true,
require: "^ngModel"
};
}).controller("WifiDeviceEditController", function($scope){
$scope.allChannels = [ "auto", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
$scope.allModes = ["802.11gn", "802.11bg", "802.11bgn", "802.11n"];
$scope.allBandwidths = [ "20", "40", "2040", "80" ];
});

View file

@ -2,6 +2,8 @@
<luci-config-section>
<luci-config-lines>
<luci-config-line title="configName"><switch ng-model="interface.disabled" class="green" inverted></switch></luci-config-line>
</luci-config-lines>
<luci-config-lines ng-hide="interface.disabled">
<luci-config-line title="'Wifi Name (SSID)'"><input type="text" class="form-control" placeholder="Wifi Name" ng-model="interface.ssid"/></luci-config-line>
<luci-config-line title="'Broadcast SSID'"><switch id="enabled" name="enabled" ng-model="interface.closed" class="green" inverted></switch></luci-config-line>
<luci-config-line title="'Frequency'">

View file

@ -11,44 +11,17 @@ $juci.module("core")
require: "^ngModel"
};
}).controller("WifiInterfaceController", function($scope){
//$scope.interface = {};
$scope.selectedProtection = "none";
$scope.selectedRadio = "wl0";
/*$scope.allFreqs = [{
label: "2.4 GHz / 5 GHz",
bands: ["a", "b"]
}, {
label: "2.4 GHz",
bands: ["b"]
}, {
label: "5 GHz",
bands: ["a"]
}]; */
$scope.allRadios = ["wl0", "wl1", "wl0wl1"];
$scope.allCryptModes = ["none", "wpa", "wpa2", "mixed-wpa", "wep-shared"];
$scope.configName = "";
// TODO: need to have generic uci variables that reflect the gui
$scope.$watch("interface", function(value){
value.disabled = (value.disabled == '1')?true:false;
value.closed = (value.closed == '1')?true:false;
//value.disabled = (value.disabled == '1')?true:false;
//value.closed = (value.closed == '1')?true:false;
$scope.configName = $scope.interface[".name"] + ".disabled";
});
/*$scope.allCryptModes = [{
label: "Off",
encryption: "none",
}, {
label: "WPA",
encryption: "wpa"
}, {
label: "WPA2",
encryption: "wpa2"
}, {
label: "WPA + WPA2",
encryption: "mixed-wpa"
}, {
label: "WEP",
encryption: "wep-shared"
}]; */
});

View file

@ -13,7 +13,7 @@
<luci-config-lines>
<luci-config-line title="'Access for listed devices'">
<div class="btn-group">
<button ng-repeat="mode in [['1','allow'], ['2', 'deny']]" class="btn btn-default" ng-model="interface.macfilter_mode" btn-radio="mode[0]">interface.macfilter.{{mode[1]}}</button>
<button ng-repeat="mode in [[1,'allow'], [2, 'deny']]" class="btn btn-default" ng-model="interface.macmode" btn-radio="mode[0]">interface.macmode.{{mode[1]}}</button>
</div>
</luci-config-line>
<luci-config-line title="'Currently added devices'">
@ -23,7 +23,7 @@
<input type="text" class="form-control" ng-model="host.hostname"/>
</td>
<td>
<input type="text" class="form-control" ng-model="host.macaddr" placeholder="xx:xx:xx:xx:xx:xx" ng-pattern="/^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$/"/>
<input type="text" class="form-control" ng-model="host.macaddr" placeholder="xx:xx:xx:xx:xx:xx" /><!-- ng-pattern="/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/"-->
</td>
<td>
<button class="btn btn-default" ng-click="onDeleteHost(host)"><i class="fa fa-trash-o"></i></button>

View file

@ -10,33 +10,37 @@ $juci.module("core")
replace: true,
require: "^ngModel"
};
}).controller("uciWirelessInterfaceMacfilterEditController", function($scope, $rpc, $uci){
}).controller("uciWirelessInterfaceMacfilterEditController", function($scope, $rpc, $uci, $hosts){
$scope.maclist = [];
$scope.filterEnabled = 0;
// updates scratch model for the view
function updateMaclist(i){
$scope.maclist = [];
var maclist = [];
if(i && i.maclist) {
var clients = $scope.clients || {};
console.log("Updating: "+JSON.stringify(i.maclist));
i.maclist.map(function(x){
var parts = x.split("|");
$scope.maclist.push({ hostname: (clients[parts[0]]||parts[1]||""), macaddr: parts[0] });
async.eachSeries(i.maclist, function(mac, next){
$hosts.select({ macaddr: mac }).done(function(host){
maclist.push(host);
next();
}).fail(function(){
$hosts.insert({ hostname: "", macaddr: mac }).done(function(host){
maclist.push(host);
}).always(function(){ next(); });
});
}, function(){
$scope.maclist = maclist;
});
} else {
$scope.maclist = [];
}
}
// watch for model change
$scope.$watch("interface", function(i){
$scope.filterEnabled = (i.macfilter && i.macfilter == "1");
});
// watch for changes in list of connected clients
$scope.$watchCollection("clients", function(clients){
updateMaclist($scope.interface);
});
$scope.filterEnabled = i.macfilter;
updateMaclist(i);
}, true);
// watch maclist for changes by the user
$scope.$watch("maclist", function(list){
@ -46,29 +50,23 @@ $juci.module("core")
interface.maclist = [];
list.map(function(x){
// save the hostname
x.macaddr = x.macaddr||"";
x.hostname = x.hostname||"";
interface.maclist.push(x.macaddr.replace("|", "")+"|"+x.hostname.replace("|", ""));
var macaddr = x.macaddr||"";
interface.maclist.push(macaddr);
});
}
}, true);
});
$scope.$watch("filterEnabled", function(value){
$scope.interface.macfilter = value;
});
$rpc.router.clients().done(function(clients){
$scope.clients = {};
$scope.client_list = [];
Object.keys(clients).map(function(x){
var cl = clients[x];
$scope.clients[cl.macaddr] = cl.hostname;
$scope.client_list.push({
$scope.client_list = Object.keys(clients).map(function(x){
return {
checked: false,
client: cl
});
client: clients[x]
}
});
console.log(JSON.stringify($scope.clients));
$scope.$apply();
});
@ -112,10 +110,6 @@ $juci.module("core")
$scope.showModal = 0;
}
$scope.validation = function(form){
}
$scope.onDismissModal = function(){
$scope.showModal = 0;
}

View file

@ -33,7 +33,7 @@ $juci.module("wifi")
});
}, function(){
$scope.info = null;
$scope.$apply();
//$scope.$apply();
});
}
$scope.onGuestEnable = function(){
@ -64,6 +64,7 @@ $juci.module("wifi")
$scope.main_wifi = $scope.interfaces[0]; //$scope.interfaces.filter(function(x) { return x[".name"] == "main"; })[0] || {};
$scope.guest_wifi = $scope.interfaces[1]; //$scope.interfaces.filter(function(x) { return x[".name"] == "guest"; })[0] || {};
console.log(JSON.stringify($scope.main_wifi));
//$scope.guestWifiEnabled = ($scope.guest_wifi && $scope.guest_wifi.up == '1');
$scope.$apply();
});

View file

@ -21,11 +21,13 @@ $juci.module("wifi")
$scope.busy = 1;
async.series([
function(next){
console.log("Saving object: "+JSON.stringify($scope.main_wifi));
$uci.set("wireless."+$scope.main_wifi[".name"], $scope.main_wifi).done(function(){
}).always(function(){ next(); });
},
function(next){
console.log("Saving object: "+JSON.stringify($scope.guest_wifi));
$uci.set("wireless."+$scope.guest_wifi[".name"], $scope.guest_wifi).done(function(){
}).always(function(){ next(); });

View file

@ -4,7 +4,9 @@
<luci-config-section>
<luci-config-heading>Settings</luci-config-heading>
<luci-config-info>Below you can change parameters for the two WiFi frequencies 2.4 GHz and 5 GHz.</luci-config-info>
<h3>2.4Ghz</h3>
<uci-wireless-device-edit ng-model="wifi24"></uci-wireless-device-edit>
<h3>5Ghz</h3>
<uci-wireless-device-edit ng-model="wifi5"></uci-wireless-device-edit>
</luci-config-section>
<luci-config-footer>

View file

@ -1,4 +1,44 @@
$juci.module("wifi")
.controller("WifiSettingsPageCtrl", function($scope){
.controller("WifiSettingsPageCtrl", function($scope, $uci){
function load(){
$uci.show("wireless").done(function(interfaces){
var list = Object.keys(interfaces)
.map(function(x){ return interfaces[x]; });
$scope.devices = list.filter(function(x) { return x[".type"] == "wifi-device"; });
$scope.interfaces = list.filter(function(x) { return x[".type"] == "wifi-iface"; });
$scope.wifi24 = $scope.devices[0]; //$scope.interfaces.filter(function(x) { return x[".name"] == "main"; })[0] || {};
$scope.wifi5 = $scope.devices[1]; //$scope.interfaces.filter(function(x) { return x[".name"] == "guest"; })[0] || {};
$scope.$apply();
});
} load();
$scope.onApply = function(){
$scope.busy = 1;
async.series([
function(next){
console.log("Saving object: "+JSON.stringify($scope.wifi24));
$uci.set("wireless."+$scope.wifi24[".name"], $scope.wifi24).done(function(){
}).always(function(){ next(); });
},
function(next){
console.log("Saving object: "+JSON.stringify($scope.wifi5));
$uci.set("wireless."+$scope.wifi5[".name"], $scope.wifi5).done(function(){
}).always(function(){ next(); });
}
], function(){
$uci.commit("wireless").done(function(){
console.log("Saved wifi settings!");
}).fail(function(){
console.log("Failed to save wifi settings!");
}).always(function(){
$scope.busy = 0;
$scope.$apply();
});
});
}
});

View file

@ -113,11 +113,7 @@
},
"uci": [
"dropbear",
"set",
"get",
"commit",
"state",
"delete"
"hosts"
]
},
"write": {
@ -129,11 +125,7 @@
},
"uci": [
"dropbear",
"set",
"get",
"commit",
"state",
"delete"
"hosts"
]
}
},