Changeset 8156

Show
Ignore:
Timestamp:
01/09/12 00:33:47 (17 months ago)
Author:
jow
Message:

libs/web: introduce recursive expression support for datatypes, introduce "or" and "and" datatypes
The commit adds a recursive parser for datatype expressions which allows nesting of validators,
this allows for complex expressions like "list(or(range(0,65535),'infinite'))" to allow a list of
values which are either integers between 0 and 65535 or the literal string "inifinite".
That change also deprecates combined datatypes like "ipaddr" or(ip4addr,ip6addr)? or
"host" or(hostname,ip4addr,ip6addr)?

Location:
luci/trunk/libs/web
Files:
3 modified

Legend:

Unmodified
Added
Removed
  • luci/trunk/libs/web/htdocs/luci-static/resources/cbi.js

    r8155 r8156  
    33 
    44    Copyright 2008 Steven Barth <steven@midlink.org> 
    5     Copyright 2008-2011 Jo-Philipp Wich <xm@subsignal.org> 
     5    Copyright 2008-2012 Jo-Philipp Wich <xm@subsignal.org> 
    66 
    77    Licensed under the Apache License, Version 2.0 (the "License"); 
     
    1818var cbi_validators = { 
    1919 
    20     'integer': function(v) 
    21     { 
    22         return (v.match(/^-?[0-9]+$/) != null); 
    23     }, 
    24  
    25     'uinteger': function(v) 
    26     { 
    27         return (cbi_validators.integer(v) && (v >= 0)); 
    28     }, 
    29  
    30     'float': function(v) 
    31     { 
    32         return !isNaN(parseFloat(v)); 
    33     }, 
    34  
    35     'ufloat': function(v) 
    36     { 
    37         return (cbi_validators['float'](v) && (v >= 0)); 
    38     }, 
    39  
    40     'ipaddr': function(v) 
    41     { 
    42         return cbi_validators.ip4addr(v) || cbi_validators.ip6addr(v); 
    43     }, 
    44  
    45     'ip4addr': function(v) 
    46     { 
    47         if (v.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/)) 
     20    'integer': function() 
     21    { 
     22        return (this.match(/^-?[0-9]+$/) != null); 
     23    }, 
     24 
     25    'uinteger': function() 
     26    { 
     27        return (cbi_validators.integer.apply(this) && (this >= 0)); 
     28    }, 
     29 
     30    'float': function() 
     31    { 
     32        return !isNaN(parseFloat(this)); 
     33    }, 
     34 
     35    'ufloat': function() 
     36    { 
     37        return (cbi_validators['float'].apply(this) && (this >= 0)); 
     38    }, 
     39 
     40    'ipaddr': function() 
     41    { 
     42        return cbi_validators.ip4addr.apply(this) || 
     43            cbi_validators.ip6addr.apply(this); 
     44    }, 
     45 
     46    'ip4addr': function() 
     47    { 
     48        if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/)) 
    4849        { 
    4950            return (RegExp.$1 >= 0) && (RegExp.$1 <= 255) && 
     
    5354                   ((RegExp.$6.indexOf('.') < 0) 
    5455                      ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32)) 
    55                       : (cbi_validators.ip4addr(RegExp.$6))) 
     56                      : (cbi_validators.ip4addr.apply(RegExp.$6))) 
    5657            ; 
    5758        } 
     
    6061    }, 
    6162 
    62     'ip6addr': function(v) 
    63     { 
    64         if( v.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/) ) 
     63    'ip6addr': function() 
     64    { 
     65        if( this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/) ) 
    6566        { 
    6667            if( !RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)) ) 
     
    7778                    var off = addr.lastIndexOf(':'); 
    7879 
    79                     if( !(off && cbi_validators.ip4addr(addr.substr(off+1))) ) 
     80                    if( !(off && cbi_validators.ip4addr.apply(addr.substr(off+1))) ) 
    8081                        return false; 
    8182 
     
    110111    }, 
    111112 
    112     'port': function(v) 
    113     { 
    114         return cbi_validators.integer(v) && (v >= 0) && (v <= 65535); 
    115     }, 
    116  
    117     'portrange': function(v) 
    118     { 
    119         if( v.match(/^(\d+)-(\d+)$/) ) 
     113    'port': function() 
     114    { 
     115        return cbi_validators.integer.apply(this) && 
     116            (this >= 0) && (this <= 65535); 
     117    }, 
     118 
     119    'portrange': function() 
     120    { 
     121        if (this.match(/^(\d+)-(\d+)$/)) 
    120122        { 
    121123            var p1 = RegExp.$1; 
    122124            var p2 = RegExp.$2; 
    123125 
    124             return cbi_validators.port(p1) && 
    125                    cbi_validators.port(p2) && 
     126            return cbi_validators.port.apply(p1) && 
     127                   cbi_validators.port.apply(p2) && 
    126128                   (parseInt(p1) <= parseInt(p2)) 
    127129            ; 
     
    129131        else 
    130132        { 
    131             return cbi_validators.port(v); 
    132         } 
    133     }, 
    134  
    135     'macaddr': function(v) 
    136     { 
    137         return (v.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null); 
    138     }, 
    139  
    140     'host': function(v) 
    141     { 
    142         return cbi_validators.hostname(v) || cbi_validators.ipaddr(v); 
    143     }, 
    144  
    145     'hostname': function(v) 
    146     { 
    147         if (v.length <= 253) 
    148             return (v.match(/^[a-zA-Z]+$/) != null || 
    149                     (v.match(/^[a-zA-Z0-9][a-zA-Z0-9\-.]*[a-zA-Z0-9]$/) && 
    150                      v.match(/[^0-9.]/))); 
     133            return cbi_validators.port.apply(this); 
     134        } 
     135    }, 
     136 
     137    'macaddr': function() 
     138    { 
     139        return (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null); 
     140    }, 
     141 
     142    'host': function() 
     143    { 
     144        return cbi_validators.hostname.apply(this) || 
     145            cbi_validators.ipaddr.apply(this); 
     146    }, 
     147 
     148    'hostname': function() 
     149    { 
     150        if (this.length <= 253) 
     151            return (this.match(/^[a-zA-Z]+$/) != null || 
     152                    (this.match(/^[a-zA-Z0-9][a-zA-Z0-9\-.]*[a-zA-Z0-9]$/) && 
     153                     this.match(/[^0-9.]/))); 
    151154 
    152155        return false; 
    153156    }, 
    154157 
    155     'network': function(v) 
    156     { 
    157         return cbi_validators.uciname(v) || cbi_validators.host(v); 
    158     }, 
    159  
    160     'wpakey': function(v) 
    161     { 
     158    'network': function() 
     159    { 
     160        return cbi_validators.uciname.apply(this) || 
     161            cbi_validators.host.apply(this); 
     162    }, 
     163 
     164    'wpakey': function() 
     165    { 
     166        var v = this; 
     167 
    162168        if( v.length == 64 ) 
    163169            return (v.match(/^[a-fA-F0-9]{64}$/) != null); 
     
    166172    }, 
    167173 
    168     'wepkey': function(v) 
    169     { 
    170         if( v.substr(0,2) == 's:' ) 
     174    'wepkey': function() 
     175    { 
     176        var v = this; 
     177 
     178        if ( v.substr(0,2) == 's:' ) 
    171179            v = v.substr(2); 
    172180 
     
    177185    }, 
    178186 
    179     'uciname': function(v) 
    180     { 
    181         return (v.match(/^[a-zA-Z0-9_]+$/) != null); 
    182     }, 
    183  
    184     'range': function(v, args) 
    185     { 
    186         var min = parseInt(args[0]); 
    187         var max = parseInt(args[1]); 
    188         var val = parseInt(v); 
    189  
     187    'uciname': function() 
     188    { 
     189        return (this.match(/^[a-zA-Z0-9_]+$/) != null); 
     190    }, 
     191 
     192    'range': function(min, max) 
     193    { 
     194        var val = parseFloat(this); 
    190195        if (!isNaN(min) && !isNaN(max) && !isNaN(val)) 
    191196            return ((val >= min) && (val <= max)); 
     
    194199    }, 
    195200 
    196     'min': function(v, args) 
    197     { 
    198         var min = parseInt(args[0]); 
    199         var val = parseInt(v); 
    200  
     201    'min': function(min) 
     202    { 
     203        var val = parseFloat(this); 
    201204        if (!isNaN(min) && !isNaN(val)) 
    202205            return (val >= min); 
     
    205208    }, 
    206209 
    207     'max': function(v, args) 
    208     { 
    209         var max = parseInt(args[0]); 
    210         var val = parseInt(v); 
    211  
     210    'max': function(max) 
     211    { 
     212        var val = parseFloat(this); 
    212213        if (!isNaN(max) && !isNaN(val)) 
    213214            return (val <= max); 
     
    216217    }, 
    217218 
    218     'neg': function(v, args) 
    219     { 
    220         if (args[0] && typeof cbi_validators[args[0]] == "function") 
    221             return cbi_validators[args[0]](v.replace(/^\s*!\s*/, '')); 
    222  
     219    'or': function() 
     220    { 
     221        for (var i = 0; i < arguments.length; i += 2) 
     222        { 
     223            if (typeof arguments[i] != 'function') 
     224            { 
     225                if (arguments[i] == this) 
     226                    return true; 
     227                i--; 
     228            } 
     229            else if (arguments[i].apply(this, arguments[i+1])) 
     230            { 
     231                return true; 
     232            } 
     233        } 
    223234        return false; 
    224235    }, 
    225236 
    226     'list': function(v, args) 
    227     { 
    228         var cb = cbi_validators[args[0] || 'string']; 
    229         if (typeof cb == "function") 
    230         { 
    231             var cbargs = args.slice(1); 
    232             var values = v.match(/[^\s]+/g); 
    233  
    234             for (var i = 0; i < values.length; i++) 
    235                 if (!cb(values[i], cbargs)) 
     237    'and': function() 
     238    { 
     239        for (var i = 0; i < arguments.length; i += 2) 
     240        { 
     241            if (typeof arguments[i] != 'function') 
     242            { 
     243                if (arguments[i] != this) 
    236244                    return false; 
    237  
    238             return true; 
    239         } 
    240  
    241         return false; 
     245                i--; 
     246            } 
     247            else if (!arguments[i].apply(this, arguments[i+1])) 
     248            { 
     249                return false; 
     250            } 
     251        } 
     252        return true; 
     253    }, 
     254 
     255    'neg': function() 
     256    { 
     257        return cbi_validators.or.apply( 
     258            this.replace(/^[ \t]*![ \t]*/, ''), arguments); 
     259    }, 
     260 
     261    'list': function(subvalidator, subargs) 
     262    { 
     263        if (typeof subvalidator != 'function') 
     264            return false; 
     265 
     266        var tokens = this.match(/[^ \t]+/g); 
     267        for (var i = 0; i < tokens.length; i++) 
     268            if (!subvalidator.apply(tokens[i], subargs)) 
     269                return false; 
     270 
     271        return true; 
    242272    } 
    243273}; 
     
    833863} 
    834864 
     865function cbi_validate_compile(code) 
     866{ 
     867    var pos = 0; 
     868    var esc = false; 
     869    var depth = 0; 
     870    var stack = [ ]; 
     871 
     872    code += ','; 
     873 
     874    for (var i = 0; i < code.length; i++) 
     875    { 
     876        if (esc) 
     877        { 
     878            esc = false; 
     879            continue; 
     880        } 
     881 
     882        switch (code.charCodeAt(i)) 
     883        { 
     884        case 92: 
     885            esc = true; 
     886            break; 
     887 
     888        case 40: 
     889        case 44: 
     890            if (depth <= 0) 
     891            { 
     892                if (pos < i) 
     893                { 
     894                    var label = code.substring(pos, i); 
     895                        label = label.replace(/\\(.)/g, '$1'); 
     896                        label = label.replace(/^[ \t]+/g, ''); 
     897                        label = label.replace(/[ \t]+$/g, ''); 
     898 
     899                    if (label && !isNaN(label)) 
     900                    { 
     901                        stack.push(parseFloat(label)); 
     902                    } 
     903                    else if (label.match(/^(['"]).*\1$/)) 
     904                    { 
     905                        stack.push(label.replace(/^(['"])(.*)\1$/, '$2')); 
     906                    } 
     907                    else if (typeof cbi_validators[label] == 'function') 
     908                    { 
     909                        stack.push(cbi_validators[label]); 
     910                        stack.push(null); 
     911                    } 
     912                    else 
     913                    { 
     914                        throw "Syntax error, unhandled token '"+label+"'"; 
     915                    } 
     916                } 
     917                pos = i+1; 
     918            } 
     919            depth += (code.charCodeAt(i) == 40); 
     920            break; 
     921 
     922        case 41: 
     923            if (--depth <= 0) 
     924            { 
     925                if (typeof stack[stack.length-2] != 'function') 
     926                    throw "Syntax error, argument list follows non-function"; 
     927 
     928                stack[stack.length-1] = 
     929                    arguments.callee(code.substring(pos, i)); 
     930 
     931                pos = i+1; 
     932            } 
     933            break; 
     934        } 
     935    } 
     936 
     937    return stack; 
     938} 
     939 
    835940function cbi_validate_field(cbid, optional, type) 
    836941{ 
    837942    var field = (typeof cbid == "string") ? document.getElementById(cbid) : cbid; 
    838     var vargs; 
    839  
    840     if( type.match(/^(\w+)\(([^\(\)]+)\)/) ) 
    841     { 
    842         type  = RegExp.$1; 
    843         vargs = RegExp.$2.split(/\s*,\s*/); 
    844     } 
    845  
    846     var vldcb = cbi_validators[type]; 
    847  
    848     if( field && vldcb ) 
     943    var vstack; try { vstack = cbi_validate_compile(type); } catch(e) { }; 
     944 
     945    if (field && vstack && typeof vstack[0] == "function") 
    849946    { 
    850947        var validator = function() 
     
    859956                    ? field.options[field.options.selectedIndex].value : field.value; 
    860957 
    861                 if( !(((value.length == 0) && optional) || vldcb(value, vargs)) ) 
     958                if (!(((value.length == 0) && optional) || vstack[0].apply(value, vstack[1]))) 
    862959                { 
    863960                    // invalid 
  • luci/trunk/libs/web/luasrc/cbi.lua

    r8124 r8156  
    153153end 
    154154 
     155-- 
     156-- Compile a datatype specification into a parse tree for evaluation later on 
     157-- 
     158local cdt_cache = { } 
     159 
     160function compile_datatype(code) 
     161    local i 
     162    local pos = 0 
     163    local esc = false 
     164    local depth = 0 
     165    local stack = { } 
     166 
     167    for i = 1, #code+1 do 
     168        local byte = code:byte(i) or 44 
     169        if esc then 
     170            esc = false 
     171        elseif byte == 92 then 
     172            esc = true 
     173        elseif byte == 40 or byte == 44 then 
     174            if depth <= 0 then 
     175                if pos < i then 
     176                    local label = code:sub(pos, i-1) 
     177                        :gsub("\\(.)", "%1") 
     178                        :gsub("^%s+", "") 
     179                        :gsub("%s+$", "") 
     180 
     181                    if #label > 0 and tonumber(label) then 
     182                        stack[#stack+1] = tonumber(label) 
     183                    elseif label:match("^'.+'$") or label:match('^".+"$') then 
     184                        stack[#stack+1] = label:gsub("[\"'](.+)[\"']", "%1") 
     185                    elseif type(datatypes[label]) == "function" then 
     186                        stack[#stack+1] = datatypes[label] 
     187                        stack[#stack+1] = { } 
     188                    else 
     189                        error("Datatype error, bad token %q" % label) 
     190                    end 
     191                end 
     192                pos = i + 1 
     193            end 
     194            depth = depth + (byte == 40 and 1 or 0) 
     195        elseif byte == 41 then 
     196            depth = depth - 1 
     197            if depth <= 0 then 
     198                if type(stack[#stack-1]) ~= "function" then 
     199                    error("Datatype error, argument list follows non-function") 
     200                end 
     201                stack[#stack] = compile_datatype(code:sub(pos, i-1)) 
     202                pos = i + 1 
     203            end 
     204        end 
     205    end 
     206 
     207    return stack 
     208end 
     209 
     210function verify_datatype(dt, value) 
     211    if dt and #dt > 0 then 
     212        if not cdt_cache[dt] then 
     213            local c = compile_datatype(dt) 
     214            if c and type(c[1]) == "function" then 
     215                cdt_cache[dt] = c 
     216            else 
     217                error("Datatype error, not a function expression") 
     218            end 
     219        end 
     220        if cdt_cache[dt] then 
     221            return cdt_cache[dt][1](value, unpack(cdt_cache[dt][2])) 
     222        end 
     223    end 
     224    return true 
     225end 
     226 
    155227 
    156228-- Node pseudo abstract class 
     
    13571429function AbstractValue.validate(self, value) 
    13581430    if self.datatype and value then 
    1359         local args = { } 
    1360         local dt, ar = self.datatype:match("^(%w+)%(([^%(%)]+)%)") 
    1361  
    1362         if dt and ar then 
    1363             local a 
    1364             for a in ar:gmatch("[^%s,]+") do 
    1365                 args[#args+1] = a 
    1366             end 
    1367         else 
    1368             dt = self.datatype 
    1369         end 
    1370  
    1371         if dt and datatypes[dt] then 
    1372             if type(value) == "table" then 
    1373                 local v 
    1374                 for _, v in ipairs(value) do 
    1375                     if v and #v > 0 and not datatypes[dt](v, unpack(args)) then 
    1376                         return nil 
    1377                     end 
    1378                 end 
    1379             else 
    1380                 if not datatypes[dt](value, unpack(args)) then 
     1431        if type(value) == "table" then 
     1432            local v 
     1433            for _, v in ipairs(value) do 
     1434                if v and #v > 0 and not verify_datatype(self.datatype, v) then 
     1435                    error('F') 
    13811436                    return nil 
    13821437                end 
     1438            end 
     1439        else 
     1440            if not verify_datatype(self.datatype, value) then 
     1441                error('F') 
     1442                return nil 
    13831443            end 
    13841444        end 
  • luci/trunk/libs/web/luasrc/cbi/datatypes.lua

    r8155 r8156  
    1818local math = require "math" 
    1919local util = require "luci.util" 
    20 local tonumber, type = tonumber, type 
     20local tonumber, type, unpack, select = tonumber, type, unpack, select 
    2121 
    2222 
    2323module "luci.cbi.datatypes" 
    2424 
     25 
     26_M['or'] = function(v, ...) 
     27    local i 
     28    for i = 1, select('#', ...), 2 do 
     29        local f = select(i, ...) 
     30        local a = select(i+1, ...) 
     31        if type(f) ~= "function" then 
     32            print("COMP", f, v) 
     33            if f == v then 
     34                return true 
     35            end 
     36            i = i - 1 
     37        elseif f(v, unpack(a)) then 
     38            return true 
     39        end 
     40    end 
     41    return false 
     42end 
     43 
     44_M['and'] = function(v, ...) 
     45    local i 
     46    for i = 1, select('#', ...), 2 do 
     47        local f = select(i, ...) 
     48        local a = select(i+1, ...) 
     49        if type(f) ~= "function" then 
     50            if f ~= v then 
     51                return false 
     52            end 
     53            i = i - 1 
     54        elseif not f(v, unpack(a)) then 
     55            return false 
     56        end 
     57    end 
     58    return true 
     59end 
     60 
     61function neg(v, ...) 
     62    return _M['or'](v:gsub("^%s*!%s*", ""), ...) 
     63end 
     64 
     65function list(v, subvalidator, subargs) 
     66    if type(subvalidator) ~= "function" then 
     67        return false 
     68    end 
     69    local token 
     70    for token in v:gmatch("%S+") do 
     71        if not subvalidator(token, unpack(subargs)) then 
     72            return false 
     73        end 
     74    end 
     75    return true 
     76end 
    2577 
    2678function bool(val) 
     
    255307    return false 
    256308end 
    257  
    258 function neg(val, what) 
    259     if what and type(_M[what]) == "function" then 
    260         return _M[what](val:gsub("^%s*!%s*", "")) 
    261     end 
    262  
    263     return false 
    264 end 
    265  
    266 function list(val, what, ...) 
    267     if type(val) == "string" and what and type(_M[what]) == "function" then 
    268         for val in val:gmatch("%S+") do 
    269             if not _M[what](val, ...) then 
    270                 return false 
    271             end 
    272         end 
    273  
    274         return true 
    275     end 
    276  
    277     return false 
    278 end