libs/cbi: don't run apply on render, this is a quick hack and will be reworked soon
[project/luci.git] / libs / cbi / luasrc / cbi.lua
index 044993a..8dd16b1 100644 (file)
@@ -29,12 +29,12 @@ module("luci.cbi", package.seeall)
 require("luci.template")
 local util = require("luci.util")
 require("luci.http")
-require("luci.uvl")
 
 
 --local event      = require "luci.sys.event"
 local fs         = require("nixio.fs")
 local uci        = require("luci.model.uci")
+local datatypes  = require("luci.cbi.datatypes")
 local class      = util.class
 local instanceof = util.instanceof
 
@@ -62,20 +62,17 @@ function load(cbimap, ...)
        local cbidir = luci.util.libpath() .. "/model/cbi/"
        local func, err
 
-       if fs.access(cbimap) then
-               func, err = loadfile(cbimap)
-       elseif fs.access(cbidir..cbimap..".lua") then
+       if fs.access(cbidir..cbimap..".lua") then
                func, err = loadfile(cbidir..cbimap..".lua")
-       elseif fs.access(cbidir..cbimap..".lua.gz") then
-               func, err = loadfile(cbidir..cbimap..".lua.gz")
+       elseif fs.access(cbimap) then
+               func, err = loadfile(cbimap)
        else
                func, err = nil, "Model '" .. cbimap .. "' not found!"
        end
 
        assert(func, err)
 
-       luci.i18n.loadc("cbi")
-       luci.i18n.loadc("uvl")
+       luci.i18n.loadc("base")
 
        local env = {
                translate=i18n.translate,
@@ -153,64 +150,6 @@ function load(cbimap, ...)
        return maps
 end
 
-local function _uvl_validate_section(node, name)
-       local co = node.map:get()
-
-       luci.uvl.STRICT_UNKNOWN_OPTIONS = false
-       luci.uvl.STRICT_UNKNOWN_SECTIONS = false
-
-       local function tag_fields(e)
-               if e.option and node.fields[e.option] then
-                       if node.fields[e.option].error then
-                               node.fields[e.option].error[name] = e
-                       else
-                               node.fields[e.option].error = { [name] = e }
-                       end
-               elseif e.childs then
-                       for _, c in ipairs(e.childs) do tag_fields(c) end
-               end
-       end
-
-       local function tag_section(e)
-               local s = { }
-               for _, c in ipairs(e.childs or { e }) do
-                       if c.childs and not c:is(luci.uvl.errors.ERR_DEPENDENCY) then
-                               table.insert( s, c.childs[1]:string() )
-                       else
-                               table.insert( s, c:string() )
-                       end
-               end
-               if #s > 0 then
-                       if node.error then
-                               node.error[name] = s
-                       else
-                               node.error = { [name] = s }
-                       end
-               end
-       end
-
-       local stat, err = node.map.validator:validate_section(node.config, name, co)
-       if err then
-               node.map.save = false
-               tag_fields(err)
-               tag_section(err)
-       end
-
-end
-
-local function _uvl_strip_remote_dependencies(deps)
-       local clean = {}
-
-       for k, v in pairs(deps) do
-               k = k:gsub("%$config%.%$section%.", "")
-               if k:match("^[%w_]+$") and type(v) == "string" then
-                       clean[k] = v
-               end
-       end
-
-       return clean
-end
-
 
 -- Node pseudo abstract class
 Node = class()
@@ -222,20 +161,23 @@ function Node.__init__(self, title, description)
        self.template = "cbi/node"
 end
 
--- i18n helper
-function Node._i18n(self, config, section, option, title, description)
-
-       -- i18n loaded?
-       if type(luci.i18n) == "table" then
-
-               local key = config and config:gsub("[^%w]+", "") or ""
-
-               if section then key = key .. "_" .. section:lower():gsub("[^%w]+", "") end
-               if option  then key = key .. "_" .. tostring(option):lower():gsub("[^%w]+", "")  end
+-- hook helper
+function Node._run_hook(self, hook)
+       if type(self[hook]) == "function" then
+               return self[hook](self)
+       end
+end
 
-               self.title = title or luci.i18n.translate( key, option or section or config )
-               self.description = description or luci.i18n.translate( key .. "_desc", "" )
+function Node._run_hooks(self, ...)
+       local f
+       local r = false
+       for _, f in ipairs(arg) do
+               if type(self[f]) == "function" then
+                       self[f](self)
+                       r = true
+               end
        end
+       return r
 end
 
 -- Prepare nodes
@@ -300,7 +242,6 @@ Map = class(Node)
 
 function Map.__init__(self, config, ...)
        Node.__init__(self, ...)
-       Node._i18n(self, config, nil, nil, ...)
 
        self.config = config
        self.parsechain = {self.config}
@@ -318,10 +259,6 @@ function Map.__init__(self, config, ...)
        if not self.uci:load(self.config) then
                error("Unable to read UCI data: " .. self.config)
        end
-
-       self.validator = luci.uvl.UVL()
-       self.scheme = self.validator:get_scheme(self.config)
-
 end
 
 function Map.formvalue(self, key)
@@ -357,6 +294,7 @@ end
 -- Use optimized UCI writing
 function Map.parse(self, readinput, ...)
        self.readinput = (readinput ~= false)
+       self:_run_hooks("on_parse")
 
        if self:formvalue("cbi.skip") then
                self.state = FORM_SKIP
@@ -369,15 +307,18 @@ function Map.parse(self, readinput, ...)
                for i, config in ipairs(self.parsechain) do
                        self.uci:save(config)
                end
-               if self:submitstate() and not self.proceed and (self.flow.autoapply or luci.http.formvalue("cbi.apply")) then
+               if self:submitstate() and ((not self.proceed and self.flow.autoapply) or luci.http.formvalue("cbi.apply")) then
+                       self:_run_hooks("on_before_commit")
                        for i, config in ipairs(self.parsechain) do
                                self.uci:commit(config)
 
                                -- Refresh data because commit changes section names
                                self.uci:load(config)
                        end
+                       self:_run_hooks("on_commit", "on_after_commit", "on_before_apply")
                        if self.apply_on_parse then
                                self.uci:apply(self.parsechain)
+                               self:_run_hooks("on_apply", "on_after_apply")
                        else
                                self._apply = function()
                                        local cmd = self.uci:apply(self.parsechain, true)
@@ -413,11 +354,13 @@ function Map.parse(self, readinput, ...)
 end
 
 function Map.render(self, ...)
+       self:_run_hooks("on_init")
        Node.render(self, ...)
-       if self._apply then
+       if false and self._apply then
                local fp = self._apply()
                fp:read("*a")
                fp:close()
+               self:_run_hooks("on_apply")
        end
 end
 
@@ -506,16 +449,13 @@ function Delegator.__init__(self, ...)
        self.pageaction = false
        self.readinput = true
        self.allow_reset = false
+       self.allow_cancel = false
        self.allow_back = false
        self.allow_finish = false
        self.template = "cbi/delegator"
 end
 
 function Delegator.set(self, name, node)
-       if type(node) == "table" and getmetatable(node) == nil then
-               node = Compound(unpack(node))
-       end
-       assert(type(node) == "function" or instanceof(node, Compound), "Invalid")
        assert(not self.nodes[name], "Duplicate entry")
 
        self.nodes[name] = node
@@ -527,9 +467,9 @@ function Delegator.add(self, name, node)
 end
 
 function Delegator.insert_after(self, name, after)
-       local n = #self.chain
+       local n = #self.chain + 1
        for k, v in ipairs(self.chain) do
-               if v == state then
+               if v == after then
                        n = k + 1
                        break
                end
@@ -555,20 +495,40 @@ function Delegator.set_route(self, ...)
 end
 
 function Delegator.get(self, name)
-       return self.nodes[name]
+       local node = self.nodes[name]
+
+       if type(node) == "string" then
+               node = load(node, name)
+       end
+
+       if type(node) == "table" and getmetatable(node) == nil then
+               node = Compound(unpack(node))
+       end
+
+       return node
 end
 
 function Delegator.parse(self, ...)
+       if self.allow_cancel and Map.formvalue(self, "cbi.cancel") then
+               if self:_run_hooks("on_cancel") then
+                       return FORM_DONE
+               end
+       end
+
+       if not Map.formvalue(self, "cbi.delg.current") then
+               self:_run_hooks("on_init")
+       end
+
        local newcurrent
        self.chain = self.chain or self:get_chain()
        self.current = self.current or self:get_active()
        self.active = self.active or self:get(self.current)
        assert(self.active, "Invalid state")
-       
+
        local stat = FORM_DONE
        if type(self.active) ~= "function" then
                self.active:populate_delegator(self)
-               stat = self.active:parse() 
+               stat = self.active:parse()
        else
                self:active()
        end
@@ -582,18 +542,24 @@ function Delegator.parse(self, ...)
        elseif stat < FORM_PROCEED then
                return stat
        end
-       
+
 
        if not Map.formvalue(self, "cbi.submit") then
                return FORM_NODATA
-       elseif not newcurrent or not self:get(newcurrent) then
-               return FORM_DONE
+       elseif stat > FORM_PROCEED
+       and (not newcurrent or not self:get(newcurrent)) then
+               return self:_run_hook("on_done") or FORM_DONE
        else
-               self.current = newcurrent
+               self.current = newcurrent or self.current
                self.active = self:get(self.current)
                if type(self.active) ~= "function" then
-                       self.active:parse(false)
-                       return FROM_PROCEED
+                       self.active:populate_delegator(self)
+                       local stat = self.active:parse(false)
+                       if stat == FORM_SKIP then
+                               return self:parse(...)
+                       else
+                               return FORM_PROCEED
+                       end
                else
                        return self:parse(...)
                end
@@ -659,6 +625,10 @@ function SimpleForm.parse(self, readinput, ...)
                return FORM_SKIP
        end
 
+       if self:formvalue("cbi.cancel") and self:_run_hooks("on_cancel") then
+               return FORM_DONE
+       end
+
        if self:submitstate() then
                Node.parse(self, 1, ...)
        end
@@ -781,27 +751,28 @@ function AbstractSection.__init__(self, map, sectiontype, ...)
        self.dynamic = false
 end
 
+-- Define a tab for the section
+function AbstractSection.tab(self, tab, title, desc)
+       self.tabs      = self.tabs      or { }
+       self.tab_names = self.tab_names or { }
+
+       self.tab_names[#self.tab_names+1] = tab
+       self.tabs[tab] = {
+               title       = title,
+               description = desc,
+               childs      = { }
+       }
+end
+
+-- Check whether the section has tabs
+function AbstractSection.has_tabs(self)
+       return (self.tabs ~= nil) and (next(self.tabs) ~= nil)
+end
+
 -- Appends a new option
 function AbstractSection.option(self, class, option, ...)
-       -- Autodetect from UVL
-       if class == true and self.map:get_scheme(self.sectiontype, option) then
-               local vs = self.map:get_scheme(self.sectiontype, option)
-               if vs.type == "boolean" then
-                       class = Flag
-               elseif vs.type == "list" then
-                       class = DynamicList
-               elseif vs.type == "enum" or vs.type == "reference" then
-                       class = ListValue
-               else
-                       class = Value
-               end
-       end
-
        if instanceof(class, AbstractValue) then
                local obj  = class(self.map, self, option, ...)
-
-               Node._i18n(obj, self.config, self.section or self.sectiontype, option, ...)
-
                self:append(obj)
                self.fields[option] = obj
                return obj
@@ -812,6 +783,31 @@ function AbstractSection.option(self, class, option, ...)
        end
 end
 
+-- Appends a new tabbed option
+function AbstractSection.taboption(self, tab, ...)
+
+       assert(tab and self.tabs and self.tabs[tab],
+               "Cannot assign option to not existing tab %q" % tostring(tab))
+
+       local l = self.tabs[tab].childs
+       local o = AbstractSection.option(self, ...)
+
+       if o then l[#l+1] = o end
+
+       return o
+end
+
+-- Render a single tab
+function AbstractSection.render_tab(self, tab, ...)
+
+       assert(tab and self.tabs and self.tabs[tab],
+               "Cannot render not existing tab %q" % tostring(tab))
+
+       for _, node in ipairs(self.tabs[tab].childs) do
+               node:render(...)
+       end
+end
+
 -- Parse optional options
 function AbstractSection.parse_optionals(self, section)
        if not self.optional then
@@ -822,7 +818,7 @@ function AbstractSection.parse_optionals(self, section)
 
        local field = self.map:formvalue("cbi.opt."..self.config.."."..section)
        for k,v in ipairs(self.children) do
-               if v.optional and not v:cfgvalue(section) then
+               if v.optional and not v:cfgvalue(section) and not self:has_tabs() then
                        if field == v.option then
                                field = nil
                                self.map.proceed = true
@@ -893,7 +889,7 @@ function AbstractSection.create(self, section)
        local stat
 
        if section then
-               stat = section:match("^%w+$") and self.map:set(section, nil, self.sectiontype)
+               stat = section:match("^[%w_]+$") and self.map:set(section, nil, self.sectiontype)
        else
                section = self.map:add(self.sectiontype)
                stat = section
@@ -991,20 +987,9 @@ NamedSection = class(AbstractSection)
 
 function NamedSection.__init__(self, map, section, stype, ...)
        AbstractSection.__init__(self, map, stype, ...)
-       Node._i18n(self, map.config, section, nil, ...)
 
        -- Defaults
        self.addremove = false
-
-       -- Use defaults from UVL
-       if not self.override_scheme and self.map:get_scheme(self.sectiontype) then
-               local vs = self.map:get_scheme(self.sectiontype)
-               self.addremove = not vs.unique and not vs.required
-               self.dynamic   = vs.dynamic
-               self.title       = self.title or vs.title
-               self.description = self.description or vs.descr
-       end
-
        self.template = "cbi/nsection"
        self.section = section
 end
@@ -1032,10 +1017,6 @@ function NamedSection.parse(self, novld)
                AbstractSection.parse_dynamic(self, s)
                if self.map:submitstate() then
                        Node.parse(self, s)
-
-                       if not novld and not self.override_scheme and self.map.scheme then
-                               _uvl_validate_section(self, s)
-                       end
                end
                AbstractSection.parse_optionals(self, s)
 
@@ -1056,21 +1037,10 @@ TypedSection = class(AbstractSection)
 
 function TypedSection.__init__(self, map, type, ...)
        AbstractSection.__init__(self, map, type, ...)
-       Node._i18n(self, map.config, type, nil, ...)
 
-       self.template  = "cbi/tsection"
+       self.template = "cbi/tsection"
        self.deps = {}
        self.anonymous = false
-
-       -- Use defaults from UVL
-       if not self.override_scheme and self.map:get_scheme(self.sectiontype) then
-               local vs = self.map:get_scheme(self.sectiontype)
-               self.addremove = not vs.unique and not vs.required
-               self.dynamic   = vs.dynamic
-               self.anonymous = not vs.named
-               self.title       = self.title or vs.title
-               self.description = self.description or vs.descr
-       end
 end
 
 -- Return all matching UCI sections for this TypedSection
@@ -1111,10 +1081,6 @@ function TypedSection.parse(self, novld)
                AbstractSection.parse_dynamic(self, k)
                if self.map:submitstate() then
                        Node.parse(self, k, novld)
-
-                       if not novld and not self.override_scheme and self.map.scheme then
-                               _uvl_validate_section(self, k)
-                       end
                end
                AbstractSection.parse_optionals(self, k)
        end
@@ -1215,6 +1181,7 @@ function AbstractValue.__init__(self, map, section, option, ...)
        self.tag_reqerror = {}
        self.tag_error = {}
        self.deps = {}
+       self.subdeps = {}
        --self.cast = "string"
 
        self.track_missing = false
@@ -1225,29 +1192,6 @@ function AbstractValue.__init__(self, map, section, option, ...)
 end
 
 function AbstractValue.prepare(self)
-       -- Use defaults from UVL
-       if not self.override_scheme
-        and self.map:get_scheme(self.section.sectiontype, self.option) then
-               local vs = self.map:get_scheme(self.section.sectiontype, self.option)
-               if self.cast == nil then
-                       self.cast = (vs.type == "list") and "list" or "string"
-               end
-               self.title       = self.title or vs.title
-               self.description = self.description or vs.descr
-               if self.default == nil then
-                       self.default = vs.default
-               end
-
-               if vs.depends and not self.override_dependencies then
-                       for i, deps in ipairs(vs.depends) do
-                               deps = _uvl_strip_remote_dependencies(deps)
-                               if next(deps) then
-                                       self:depends(deps)
-                               end
-                       end
-               end
-       end
-
        self.cast = self.cast or "string"
 end
 
@@ -1292,6 +1236,22 @@ function AbstractValue.parse(self, section, novld)
        local fvalue = self:formvalue(section)
        local cvalue = self:cfgvalue(section)
 
+       -- If favlue and cvalue are both tables and have the same content
+       -- make them identical
+       if type(fvalue) == "table" and type(cvalue) == "table" then
+               local equal = #fvalue == #cvalue
+               if equal then
+                       for i=1, #fvalue do
+                               if cvalue[i] ~= fvalue[i] then
+                                       equal = false
+                               end
+                       end
+               end
+               if equal then
+                       fvalue = cvalue
+               end
+       end
+
        if fvalue and #fvalue > 0 then -- If we have a form value, write it to UCI
                fvalue = self:transform(self:validate(fvalue, section))
                if not fvalue and not novld then
@@ -1304,7 +1264,7 @@ function AbstractValue.parse(self, section, novld)
                                table.insert(self.section.error[section], "invalid")
                        else
                                self.section.error = {[section] = {"invalid"}}
-                       end 
+                       end
                        self.map.save = false
                end
                if fvalue and not (fvalue == cvalue) then
@@ -1335,7 +1295,7 @@ end
 
 -- Render if this value exists or if it is mandatory
 function AbstractValue.render(self, s, scope)
-       if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
+       if not self.optional or self.section:has_tabs() or self:cfgvalue(s) or self:formcreated(s) then
                scope = scope or {}
                scope.section   = s
                scope.cbid      = self:cbid(s)
@@ -1366,7 +1326,8 @@ end
 
 -- Return the UCI value of this object
 function AbstractValue.cfgvalue(self, section)
-       local value = self.map:get(section, self.option)
+       local value = (self.error and self.error[section] == "invalid")
+               and self:formvalue(section) or self.map:get(section, self.option)
        if not value then
                return nil
        elseif not self.cast or self.cast == type(value) then
@@ -1382,6 +1343,20 @@ end
 
 -- Validate the form value
 function AbstractValue.validate(self, value)
+       if self.datatype and value and datatypes[self.datatype] then
+               if type(value) == "table" then
+                       local v
+                       for _, v in ipairs(value) do
+                               if v and #v > 0 and not datatypes[self.datatype](v) then
+                                       return nil
+                               end
+                       end
+               else
+                       if not datatypes[self.datatype](value) then
+                               return nil
+                       end
+               end
+       end
        return value
 end
 
@@ -1499,26 +1474,6 @@ function ListValue.__init__(self, ...)
        self.widget = "select"
 end
 
-function ListValue.prepare(self, ...)
-       AbstractValue.prepare(self, ...)
-       if not self.override_scheme
-        and self.map:get_scheme(self.section.sectiontype, self.option) then
-               local vs = self.map:get_scheme(self.section.sectiontype, self.option)
-               if self.value and vs.valuelist and not self.override_values then
-                       for k, v in ipairs(vs.valuelist) do
-                               local deps = {}
-                               if not self.override_dependencies
-                                and vs.enum_depends and vs.enum_depends[v.value] then
-                                       for i, dep in ipairs(vs.enum_depends[v.value]) do
-                                               table.insert(deps, _uvl_strip_remote_dependencies(dep))
-                                       end
-                               end
-                               self:value(v.value, v.title or v.value, unpack(deps))
-                       end
-               end
-       end
-end
-
 function ListValue.value(self, key, val, ...)
        if luci.util.contains(self.keylist, key) then
                return
@@ -1529,7 +1484,7 @@ function ListValue.value(self, key, val, ...)
        table.insert(self.vallist, tostring(val))
 
        for i, deps in ipairs({...}) do
-               table.insert(self.deps, {add = "-"..key, deps=deps})
+               self.subdeps[#self.subdeps + 1] = {add = "-"..key, deps=deps}
        end
 end