Do not render standard widgets like checkboxes, select boxes, text input fields etc. on the server side anymore but utilize the ui.js primitives instead. This avoids logic duplication between server side cbi templates and JS widgets in the future. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
1977 lines
42 KiB
Lua
1977 lines
42 KiB
Lua
-- Copyright 2008 Steven Barth <steven@midlink.org>
|
|
-- Licensed to the public under the Apache License 2.0.
|
|
|
|
module("luci.cbi", package.seeall)
|
|
|
|
require("luci.template")
|
|
local util = require("luci.util")
|
|
require("luci.http")
|
|
|
|
|
|
--local event = require "luci.sys.event"
|
|
local fs = require("nixio.fs")
|
|
local uci = require("luci.model.uci")
|
|
local datatypes = require("luci.cbi.datatypes")
|
|
local dispatcher = require("luci.dispatcher")
|
|
local class = util.class
|
|
local instanceof = util.instanceof
|
|
|
|
FORM_NODATA = 0
|
|
FORM_PROCEED = 0
|
|
FORM_VALID = 1
|
|
FORM_DONE = 1
|
|
FORM_INVALID = -1
|
|
FORM_CHANGED = 2
|
|
FORM_SKIP = 4
|
|
|
|
AUTO = true
|
|
|
|
CREATE_PREFIX = "cbi.cts."
|
|
REMOVE_PREFIX = "cbi.rts."
|
|
RESORT_PREFIX = "cbi.sts."
|
|
FEXIST_PREFIX = "cbi.cbe."
|
|
|
|
-- Loads a CBI map from given file, creating an environment and returns it
|
|
function load(cbimap, ...)
|
|
local fs = require "nixio.fs"
|
|
local i18n = require "luci.i18n"
|
|
require("luci.config")
|
|
require("luci.util")
|
|
|
|
local upldir = "/etc/luci-uploads/"
|
|
local cbidir = luci.util.libpath() .. "/model/cbi/"
|
|
local func, err
|
|
|
|
if fs.access(cbidir..cbimap..".lua") then
|
|
func, err = loadfile(cbidir..cbimap..".lua")
|
|
elseif fs.access(cbimap) then
|
|
func, err = loadfile(cbimap)
|
|
else
|
|
func, err = nil, "Model '" .. cbimap .. "' not found!"
|
|
end
|
|
|
|
assert(func, err)
|
|
|
|
local env = {
|
|
translate=i18n.translate,
|
|
translatef=i18n.translatef,
|
|
arg={...}
|
|
}
|
|
|
|
setfenv(func, setmetatable(env, {__index =
|
|
function(tbl, key)
|
|
return rawget(tbl, key) or _M[key] or _G[key]
|
|
end}))
|
|
|
|
local maps = { func() }
|
|
local uploads = { }
|
|
local has_upload = false
|
|
|
|
for i, map in ipairs(maps) do
|
|
if not instanceof(map, Node) then
|
|
error("CBI map returns no valid map object!")
|
|
return nil
|
|
else
|
|
map:prepare()
|
|
if map.upload_fields then
|
|
has_upload = true
|
|
for _, field in ipairs(map.upload_fields) do
|
|
uploads[
|
|
field.config .. '.' ..
|
|
(field.section.sectiontype or '1') .. '.' ..
|
|
field.option
|
|
] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if has_upload then
|
|
local uci = luci.model.uci.cursor()
|
|
local prm = luci.http.context.request.message.params
|
|
local fd, cbid
|
|
|
|
luci.http.setfilehandler(
|
|
function( field, chunk, eof )
|
|
if not field then return end
|
|
if field.name and not cbid then
|
|
local c, s, o = field.name:gmatch(
|
|
"cbid%.([^%.]+)%.([^%.]+)%.([^%.]+)"
|
|
)()
|
|
|
|
if c and s and o then
|
|
local t = uci:get( c, s ) or s
|
|
if uploads[c.."."..t.."."..o] then
|
|
local path = upldir .. field.name
|
|
fd = io.open(path, "w")
|
|
if fd then
|
|
cbid = field.name
|
|
prm[cbid] = path
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if field.name == cbid and fd then
|
|
fd:write(chunk)
|
|
end
|
|
|
|
if eof and fd then
|
|
fd:close()
|
|
fd = nil
|
|
cbid = nil
|
|
end
|
|
end
|
|
)
|
|
end
|
|
|
|
return maps
|
|
end
|
|
|
|
--
|
|
-- Compile a datatype specification into a parse tree for evaluation later on
|
|
--
|
|
local cdt_cache = { }
|
|
|
|
function compile_datatype(code)
|
|
local i
|
|
local pos = 0
|
|
local esc = false
|
|
local depth = 0
|
|
local stack = { }
|
|
|
|
for i = 1, #code+1 do
|
|
local byte = code:byte(i) or 44
|
|
if esc then
|
|
esc = false
|
|
elseif byte == 92 then
|
|
esc = true
|
|
elseif byte == 40 or byte == 44 then
|
|
if depth <= 0 then
|
|
if pos < i then
|
|
local label = code:sub(pos, i-1)
|
|
:gsub("\\(.)", "%1")
|
|
:gsub("^%s+", "")
|
|
:gsub("%s+$", "")
|
|
|
|
if #label > 0 and tonumber(label) then
|
|
stack[#stack+1] = tonumber(label)
|
|
elseif label:match("^'.*'$") or label:match('^".*"$') then
|
|
stack[#stack+1] = label:gsub("[\"'](.*)[\"']", "%1")
|
|
elseif type(datatypes[label]) == "function" then
|
|
stack[#stack+1] = datatypes[label]
|
|
stack[#stack+1] = { }
|
|
else
|
|
error("Datatype error, bad token %q" % label)
|
|
end
|
|
end
|
|
pos = i + 1
|
|
end
|
|
depth = depth + (byte == 40 and 1 or 0)
|
|
elseif byte == 41 then
|
|
depth = depth - 1
|
|
if depth <= 0 then
|
|
if type(stack[#stack-1]) ~= "function" then
|
|
error("Datatype error, argument list follows non-function")
|
|
end
|
|
stack[#stack] = compile_datatype(code:sub(pos, i-1))
|
|
pos = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
return stack
|
|
end
|
|
|
|
function verify_datatype(dt, value)
|
|
if dt and #dt > 0 then
|
|
if not cdt_cache[dt] then
|
|
local c = compile_datatype(dt)
|
|
if c and type(c[1]) == "function" then
|
|
cdt_cache[dt] = c
|
|
else
|
|
error("Datatype error, not a function expression")
|
|
end
|
|
end
|
|
if cdt_cache[dt] then
|
|
return cdt_cache[dt][1](value, unpack(cdt_cache[dt][2]))
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
|
|
-- Node pseudo abstract class
|
|
Node = class()
|
|
|
|
function Node.__init__(self, title, description)
|
|
self.children = {}
|
|
self.title = title or ""
|
|
self.description = description or ""
|
|
self.template = "cbi/node"
|
|
end
|
|
|
|
-- hook helper
|
|
function Node._run_hook(self, hook)
|
|
if type(self[hook]) == "function" then
|
|
return self[hook](self)
|
|
end
|
|
end
|
|
|
|
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
|
|
function Node.prepare(self, ...)
|
|
for k, child in ipairs(self.children) do
|
|
child:prepare(...)
|
|
end
|
|
end
|
|
|
|
-- Append child nodes
|
|
function Node.append(self, obj)
|
|
table.insert(self.children, obj)
|
|
end
|
|
|
|
-- Parse this node and its children
|
|
function Node.parse(self, ...)
|
|
for k, child in ipairs(self.children) do
|
|
child:parse(...)
|
|
end
|
|
end
|
|
|
|
-- Render this node
|
|
function Node.render(self, scope)
|
|
scope = scope or {}
|
|
scope.self = self
|
|
|
|
luci.template.render(self.template, scope)
|
|
end
|
|
|
|
-- Render the children
|
|
function Node.render_children(self, ...)
|
|
local k, node
|
|
for k, node in ipairs(self.children) do
|
|
node.last_child = (k == #self.children)
|
|
node.index = k
|
|
node:render(...)
|
|
end
|
|
end
|
|
|
|
|
|
--[[
|
|
A simple template element
|
|
]]--
|
|
Template = class(Node)
|
|
|
|
function Template.__init__(self, template)
|
|
Node.__init__(self)
|
|
self.template = template
|
|
end
|
|
|
|
function Template.render(self)
|
|
luci.template.render(self.template, {self=self})
|
|
end
|
|
|
|
function Template.parse(self, readinput)
|
|
self.readinput = (readinput ~= false)
|
|
return Map.formvalue(self, "cbi.submit") and FORM_DONE or FORM_NODATA
|
|
end
|
|
|
|
|
|
--[[
|
|
Map - A map describing a configuration file
|
|
]]--
|
|
Map = class(Node)
|
|
|
|
function Map.__init__(self, config, ...)
|
|
Node.__init__(self, ...)
|
|
|
|
self.config = config
|
|
self.parsechain = {self.config}
|
|
self.template = "cbi/map"
|
|
self.apply_on_parse = nil
|
|
self.readinput = true
|
|
self.proceed = false
|
|
self.flow = {}
|
|
|
|
self.uci = uci.cursor()
|
|
self.save = true
|
|
|
|
self.changed = false
|
|
|
|
local path = "%s/%s" %{ self.uci:get_confdir(), self.config }
|
|
if fs.stat(path, "type") ~= "reg" then
|
|
fs.writefile(path, "")
|
|
end
|
|
|
|
local ok, err = self.uci:load(self.config)
|
|
if not ok then
|
|
local url = dispatcher.build_url(unpack(dispatcher.context.request))
|
|
local source = self:formvalue("cbi.source")
|
|
if type(source) == "string" then
|
|
fs.writefile(path, source:gsub("\r\n", "\n"))
|
|
ok, err = self.uci:load(self.config)
|
|
if ok then
|
|
luci.http.redirect(url)
|
|
end
|
|
end
|
|
self.save = false
|
|
end
|
|
|
|
if not ok then
|
|
self.template = "cbi/error"
|
|
self.error = err
|
|
self.source = fs.readfile(path) or ""
|
|
self.pageaction = false
|
|
end
|
|
end
|
|
|
|
function Map.formvalue(self, key)
|
|
return self.readinput and luci.http.formvalue(key) or nil
|
|
end
|
|
|
|
function Map.formvaluetable(self, key)
|
|
return self.readinput and luci.http.formvaluetable(key) or {}
|
|
end
|
|
|
|
function Map.get_scheme(self, sectiontype, option)
|
|
if not option then
|
|
return self.scheme and self.scheme.sections[sectiontype]
|
|
else
|
|
return self.scheme and self.scheme.variables[sectiontype]
|
|
and self.scheme.variables[sectiontype][option]
|
|
end
|
|
end
|
|
|
|
function Map.submitstate(self)
|
|
return self:formvalue("cbi.submit")
|
|
end
|
|
|
|
-- Chain foreign config
|
|
function Map.chain(self, config)
|
|
table.insert(self.parsechain, config)
|
|
end
|
|
|
|
function Map.state_handler(self, state)
|
|
return state
|
|
end
|
|
|
|
-- Use optimized UCI writing
|
|
function Map.parse(self, readinput, ...)
|
|
if self:formvalue("cbi.skip") then
|
|
self.state = FORM_SKIP
|
|
elseif not self.save then
|
|
self.state = FORM_INVALID
|
|
elseif not self:submitstate() then
|
|
self.state = FORM_NODATA
|
|
end
|
|
|
|
-- Back out early to prevent unauthorized changes on the subsequent parse
|
|
if self.state ~= nil then
|
|
return self:state_handler(self.state)
|
|
end
|
|
|
|
self.readinput = (readinput ~= false)
|
|
self:_run_hooks("on_parse")
|
|
|
|
Node.parse(self, ...)
|
|
|
|
if self.save then
|
|
self:_run_hooks("on_save", "on_before_save")
|
|
local i, config
|
|
for i, config in ipairs(self.parsechain) do
|
|
self.uci:save(config)
|
|
end
|
|
self:_run_hooks("on_after_save")
|
|
if (not self.proceed and self.flow.autoapply) or luci.http.formvalue("cbi.apply") then
|
|
self:_run_hooks("on_before_commit")
|
|
if self.apply_on_parse == false then
|
|
for i, config in ipairs(self.parsechain) do
|
|
self.uci:commit(config)
|
|
end
|
|
end
|
|
self:_run_hooks("on_commit", "on_after_commit", "on_before_apply")
|
|
if self.apply_on_parse == true or self.apply_on_parse == false then
|
|
self.uci:apply(self.apply_on_parse)
|
|
self:_run_hooks("on_apply", "on_after_apply")
|
|
else
|
|
-- This is evaluated by the dispatcher and delegated to the
|
|
-- template which in turn fires XHR to perform the actual
|
|
-- apply actions.
|
|
self.apply_needed = true
|
|
end
|
|
|
|
-- Reparse sections
|
|
Node.parse(self, true)
|
|
end
|
|
for i, config in ipairs(self.parsechain) do
|
|
self.uci:unload(config)
|
|
end
|
|
if type(self.commit_handler) == "function" then
|
|
self:commit_handler(self:submitstate())
|
|
end
|
|
end
|
|
|
|
if not self.save then
|
|
self.state = FORM_INVALID
|
|
elseif self.proceed then
|
|
self.state = FORM_PROCEED
|
|
elseif self.changed then
|
|
self.state = FORM_CHANGED
|
|
else
|
|
self.state = FORM_VALID
|
|
end
|
|
|
|
return self:state_handler(self.state)
|
|
end
|
|
|
|
function Map.render(self, ...)
|
|
self:_run_hooks("on_init")
|
|
Node.render(self, ...)
|
|
end
|
|
|
|
-- Creates a child section
|
|
function Map.section(self, class, ...)
|
|
if instanceof(class, AbstractSection) then
|
|
local obj = class(self, ...)
|
|
self:append(obj)
|
|
return obj
|
|
else
|
|
error("class must be a descendent of AbstractSection")
|
|
end
|
|
end
|
|
|
|
-- UCI add
|
|
function Map.add(self, sectiontype)
|
|
return self.uci:add(self.config, sectiontype)
|
|
end
|
|
|
|
-- UCI set
|
|
function Map.set(self, section, option, value)
|
|
if type(value) ~= "table" or #value > 0 then
|
|
if option then
|
|
return self.uci:set(self.config, section, option, value)
|
|
else
|
|
return self.uci:set(self.config, section, value)
|
|
end
|
|
else
|
|
return Map.del(self, section, option)
|
|
end
|
|
end
|
|
|
|
-- UCI del
|
|
function Map.del(self, section, option)
|
|
if option then
|
|
return self.uci:delete(self.config, section, option)
|
|
else
|
|
return self.uci:delete(self.config, section)
|
|
end
|
|
end
|
|
|
|
-- UCI get
|
|
function Map.get(self, section, option)
|
|
if not section then
|
|
return self.uci:get_all(self.config)
|
|
elseif option then
|
|
return self.uci:get(self.config, section, option)
|
|
else
|
|
return self.uci:get_all(self.config, section)
|
|
end
|
|
end
|
|
|
|
--[[
|
|
Compound - Container
|
|
]]--
|
|
Compound = class(Node)
|
|
|
|
function Compound.__init__(self, ...)
|
|
Node.__init__(self)
|
|
self.template = "cbi/compound"
|
|
self.children = {...}
|
|
end
|
|
|
|
function Compound.populate_delegator(self, delegator)
|
|
for _, v in ipairs(self.children) do
|
|
v.delegator = delegator
|
|
end
|
|
end
|
|
|
|
function Compound.parse(self, ...)
|
|
local cstate, state = 0
|
|
|
|
for k, child in ipairs(self.children) do
|
|
cstate = child:parse(...)
|
|
state = (not state or cstate < state) and cstate or state
|
|
end
|
|
|
|
return state
|
|
end
|
|
|
|
|
|
--[[
|
|
Delegator - Node controller
|
|
]]--
|
|
Delegator = class(Node)
|
|
function Delegator.__init__(self, ...)
|
|
Node.__init__(self, ...)
|
|
self.nodes = {}
|
|
self.defaultpath = {}
|
|
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)
|
|
assert(not self.nodes[name], "Duplicate entry")
|
|
|
|
self.nodes[name] = node
|
|
end
|
|
|
|
function Delegator.add(self, name, node)
|
|
node = self:set(name, node)
|
|
self.defaultpath[#self.defaultpath+1] = name
|
|
end
|
|
|
|
function Delegator.insert_after(self, name, after)
|
|
local n = #self.chain + 1
|
|
for k, v in ipairs(self.chain) do
|
|
if v == after then
|
|
n = k + 1
|
|
break
|
|
end
|
|
end
|
|
table.insert(self.chain, n, name)
|
|
end
|
|
|
|
function Delegator.set_route(self, ...)
|
|
local n, chain, route = 0, self.chain, {...}
|
|
for i = 1, #chain do
|
|
if chain[i] == self.current then
|
|
n = i
|
|
break
|
|
end
|
|
end
|
|
for i = 1, #route do
|
|
n = n + 1
|
|
chain[n] = route[i]
|
|
end
|
|
for i = n + 1, #chain do
|
|
chain[i] = nil
|
|
end
|
|
end
|
|
|
|
function Delegator.get(self, 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()
|
|
else
|
|
self:active()
|
|
end
|
|
|
|
if stat > FORM_PROCEED then
|
|
if Map.formvalue(self, "cbi.delg.back") then
|
|
newcurrent = self:get_prev(self.current)
|
|
else
|
|
newcurrent = self:get_next(self.current)
|
|
end
|
|
elseif stat < FORM_PROCEED then
|
|
return stat
|
|
end
|
|
|
|
|
|
if not Map.formvalue(self, "cbi.submit") then
|
|
return FORM_NODATA
|
|
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 or self.current
|
|
self.active = self:get(self.current)
|
|
if type(self.active) ~= "function" then
|
|
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
|
|
end
|
|
end
|
|
|
|
function Delegator.get_next(self, state)
|
|
for k, v in ipairs(self.chain) do
|
|
if v == state then
|
|
return self.chain[k+1]
|
|
end
|
|
end
|
|
end
|
|
|
|
function Delegator.get_prev(self, state)
|
|
for k, v in ipairs(self.chain) do
|
|
if v == state then
|
|
return self.chain[k-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
function Delegator.get_chain(self)
|
|
local x = Map.formvalue(self, "cbi.delg.path") or self.defaultpath
|
|
return type(x) == "table" and x or {x}
|
|
end
|
|
|
|
function Delegator.get_active(self)
|
|
return Map.formvalue(self, "cbi.delg.current") or self.chain[1]
|
|
end
|
|
|
|
--[[
|
|
Page - A simple node
|
|
]]--
|
|
|
|
Page = class(Node)
|
|
Page.__init__ = Node.__init__
|
|
Page.parse = function() end
|
|
|
|
|
|
--[[
|
|
SimpleForm - A Simple non-UCI form
|
|
]]--
|
|
SimpleForm = class(Node)
|
|
|
|
function SimpleForm.__init__(self, config, title, description, data)
|
|
Node.__init__(self, title, description)
|
|
self.config = config
|
|
self.data = data or {}
|
|
self.template = "cbi/simpleform"
|
|
self.dorender = true
|
|
self.pageaction = false
|
|
self.readinput = true
|
|
end
|
|
|
|
SimpleForm.formvalue = Map.formvalue
|
|
SimpleForm.formvaluetable = Map.formvaluetable
|
|
|
|
function SimpleForm.parse(self, readinput, ...)
|
|
self.readinput = (readinput ~= false)
|
|
|
|
if self:formvalue("cbi.skip") then
|
|
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
|
|
|
|
local valid = true
|
|
for k, j in ipairs(self.children) do
|
|
for i, v in ipairs(j.children) do
|
|
valid = valid
|
|
and (not v.tag_missing or not v.tag_missing[1])
|
|
and (not v.tag_invalid or not v.tag_invalid[1])
|
|
and (not v.error)
|
|
end
|
|
end
|
|
|
|
local state =
|
|
not self:submitstate() and FORM_NODATA
|
|
or valid and FORM_VALID
|
|
or FORM_INVALID
|
|
|
|
self.dorender = not self.handle
|
|
if self.handle then
|
|
local nrender, nstate = self:handle(state, self.data)
|
|
self.dorender = self.dorender or (nrender ~= false)
|
|
state = nstate or state
|
|
end
|
|
return state
|
|
end
|
|
|
|
function SimpleForm.render(self, ...)
|
|
if self.dorender then
|
|
Node.render(self, ...)
|
|
end
|
|
end
|
|
|
|
function SimpleForm.submitstate(self)
|
|
return self:formvalue("cbi.submit")
|
|
end
|
|
|
|
function SimpleForm.section(self, class, ...)
|
|
if instanceof(class, AbstractSection) then
|
|
local obj = class(self, ...)
|
|
self:append(obj)
|
|
return obj
|
|
else
|
|
error("class must be a descendent of AbstractSection")
|
|
end
|
|
end
|
|
|
|
-- Creates a child field
|
|
function SimpleForm.field(self, class, ...)
|
|
local section
|
|
for k, v in ipairs(self.children) do
|
|
if instanceof(v, SimpleSection) then
|
|
section = v
|
|
break
|
|
end
|
|
end
|
|
if not section then
|
|
section = self:section(SimpleSection)
|
|
end
|
|
|
|
if instanceof(class, AbstractValue) then
|
|
local obj = class(self, section, ...)
|
|
obj.track_missing = true
|
|
section:append(obj)
|
|
return obj
|
|
else
|
|
error("class must be a descendent of AbstractValue")
|
|
end
|
|
end
|
|
|
|
function SimpleForm.set(self, section, option, value)
|
|
self.data[option] = value
|
|
end
|
|
|
|
|
|
function SimpleForm.del(self, section, option)
|
|
self.data[option] = nil
|
|
end
|
|
|
|
|
|
function SimpleForm.get(self, section, option)
|
|
return self.data[option]
|
|
end
|
|
|
|
|
|
function SimpleForm.get_scheme()
|
|
return nil
|
|
end
|
|
|
|
|
|
Form = class(SimpleForm)
|
|
|
|
function Form.__init__(self, ...)
|
|
SimpleForm.__init__(self, ...)
|
|
self.embedded = true
|
|
end
|
|
|
|
|
|
--[[
|
|
AbstractSection
|
|
]]--
|
|
AbstractSection = class(Node)
|
|
|
|
function AbstractSection.__init__(self, map, sectiontype, ...)
|
|
Node.__init__(self, ...)
|
|
self.sectiontype = sectiontype
|
|
self.map = map
|
|
self.config = map.config
|
|
self.optionals = {}
|
|
self.defaults = {}
|
|
self.fields = {}
|
|
self.tag_error = {}
|
|
self.tag_invalid = {}
|
|
self.tag_deperror = {}
|
|
self.changed = false
|
|
|
|
self.optional = true
|
|
self.addremove = false
|
|
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, ...)
|
|
if instanceof(class, AbstractValue) then
|
|
local obj = class(self.map, self, option, ...)
|
|
self:append(obj)
|
|
self.fields[option] = obj
|
|
return obj
|
|
elseif class == true then
|
|
error("No valid class was given and autodetection failed.")
|
|
else
|
|
error("class must be a descendant of AbstractValue")
|
|
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))
|
|
|
|
local k, node
|
|
for k, node in ipairs(self.tabs[tab].childs) do
|
|
node.last_child = (k == #self.tabs[tab].childs)
|
|
node.index = k
|
|
node:render(...)
|
|
end
|
|
end
|
|
|
|
-- Parse optional options
|
|
function AbstractSection.parse_optionals(self, section, noparse)
|
|
if not self.optional then
|
|
return
|
|
end
|
|
|
|
self.optionals[section] = {}
|
|
|
|
local field = nil
|
|
if not noparse then
|
|
field = self.map:formvalue("cbi.opt."..self.config.."."..section)
|
|
end
|
|
|
|
for k,v in ipairs(self.children) do
|
|
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
|
|
else
|
|
table.insert(self.optionals[section], v)
|
|
end
|
|
end
|
|
end
|
|
|
|
if field and #field > 0 and self.dynamic then
|
|
self:add_dynamic(field)
|
|
end
|
|
end
|
|
|
|
-- Add a dynamic option
|
|
function AbstractSection.add_dynamic(self, field, optional)
|
|
local o = self:option(Value, field, field)
|
|
o.optional = optional
|
|
end
|
|
|
|
-- Parse all dynamic options
|
|
function AbstractSection.parse_dynamic(self, section)
|
|
if not self.dynamic then
|
|
return
|
|
end
|
|
|
|
local arr = luci.util.clone(self:cfgvalue(section))
|
|
local form = self.map:formvaluetable("cbid."..self.config.."."..section)
|
|
for k, v in pairs(form) do
|
|
arr[k] = v
|
|
end
|
|
|
|
for key,val in pairs(arr) do
|
|
local create = true
|
|
|
|
for i,c in ipairs(self.children) do
|
|
if c.option == key then
|
|
create = false
|
|
end
|
|
end
|
|
|
|
if create and key:sub(1, 1) ~= "." then
|
|
self.map.proceed = true
|
|
self:add_dynamic(key, true)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Returns the section's UCI table
|
|
function AbstractSection.cfgvalue(self, section)
|
|
return self.map:get(section)
|
|
end
|
|
|
|
-- Push events
|
|
function AbstractSection.push_events(self)
|
|
--luci.util.append(self.map.events, self.events)
|
|
self.map.changed = true
|
|
end
|
|
|
|
-- Removes the section
|
|
function AbstractSection.remove(self, section)
|
|
self.map.proceed = true
|
|
return self.map:del(section)
|
|
end
|
|
|
|
-- Creates the section
|
|
function AbstractSection.create(self, section)
|
|
local stat
|
|
|
|
if section then
|
|
stat = section:match("^[%w_]+$") and self.map:set(section, nil, self.sectiontype)
|
|
else
|
|
section = self.map:add(self.sectiontype)
|
|
stat = section
|
|
end
|
|
|
|
if stat then
|
|
for k,v in pairs(self.children) do
|
|
if v.default then
|
|
self.map:set(section, v.option, v.default)
|
|
end
|
|
end
|
|
|
|
for k,v in pairs(self.defaults) do
|
|
self.map:set(section, k, v)
|
|
end
|
|
end
|
|
|
|
self.map.proceed = true
|
|
|
|
return stat
|
|
end
|
|
|
|
|
|
SimpleSection = class(AbstractSection)
|
|
|
|
function SimpleSection.__init__(self, form, ...)
|
|
AbstractSection.__init__(self, form, nil, ...)
|
|
self.template = "cbi/nullsection"
|
|
end
|
|
|
|
|
|
Table = class(AbstractSection)
|
|
|
|
function Table.__init__(self, form, data, ...)
|
|
local datasource = {}
|
|
local tself = self
|
|
datasource.config = "table"
|
|
self.data = data or {}
|
|
|
|
datasource.formvalue = Map.formvalue
|
|
datasource.formvaluetable = Map.formvaluetable
|
|
datasource.readinput = true
|
|
|
|
function datasource.get(self, section, option)
|
|
return tself.data[section] and tself.data[section][option]
|
|
end
|
|
|
|
function datasource.submitstate(self)
|
|
return Map.formvalue(self, "cbi.submit")
|
|
end
|
|
|
|
function datasource.del(...)
|
|
return true
|
|
end
|
|
|
|
function datasource.get_scheme()
|
|
return nil
|
|
end
|
|
|
|
AbstractSection.__init__(self, datasource, "table", ...)
|
|
self.template = "cbi/tblsection"
|
|
self.rowcolors = true
|
|
self.anonymous = true
|
|
end
|
|
|
|
function Table.parse(self, readinput)
|
|
self.map.readinput = (readinput ~= false)
|
|
for i, k in ipairs(self:cfgsections()) do
|
|
if self.map:submitstate() then
|
|
Node.parse(self, k)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Table.cfgsections(self)
|
|
local sections = {}
|
|
|
|
for i, v in luci.util.kspairs(self.data) do
|
|
table.insert(sections, i)
|
|
end
|
|
|
|
return sections
|
|
end
|
|
|
|
function Table.update(self, data)
|
|
self.data = data
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
NamedSection - A fixed configuration section defined by its name
|
|
]]--
|
|
NamedSection = class(AbstractSection)
|
|
|
|
function NamedSection.__init__(self, map, section, stype, ...)
|
|
AbstractSection.__init__(self, map, stype, ...)
|
|
|
|
-- Defaults
|
|
self.addremove = false
|
|
self.template = "cbi/nsection"
|
|
self.section = section
|
|
end
|
|
|
|
function NamedSection.prepare(self)
|
|
AbstractSection.prepare(self)
|
|
AbstractSection.parse_optionals(self, self.section, true)
|
|
end
|
|
|
|
function NamedSection.parse(self, novld)
|
|
local s = self.section
|
|
local active = self:cfgvalue(s)
|
|
|
|
if self.addremove then
|
|
local path = self.config.."."..s
|
|
if active then -- Remove the section
|
|
if self.map:formvalue("cbi.rns."..path) and self:remove(s) then
|
|
self:push_events()
|
|
return
|
|
end
|
|
else -- Create and apply default values
|
|
if self.map:formvalue("cbi.cns."..path) then
|
|
self:create(s)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
if active then
|
|
AbstractSection.parse_dynamic(self, s)
|
|
if self.map:submitstate() then
|
|
Node.parse(self, s)
|
|
end
|
|
AbstractSection.parse_optionals(self, s)
|
|
|
|
if self.changed then
|
|
self:push_events()
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
--[[
|
|
TypedSection - A (set of) configuration section(s) defined by the type
|
|
addremove: Defines whether the user can add/remove sections of this type
|
|
anonymous: Allow creating anonymous sections
|
|
validate: a validation function returning nil if the section is invalid
|
|
]]--
|
|
TypedSection = class(AbstractSection)
|
|
|
|
function TypedSection.__init__(self, map, type, ...)
|
|
AbstractSection.__init__(self, map, type, ...)
|
|
|
|
self.template = "cbi/tsection"
|
|
self.deps = {}
|
|
self.anonymous = false
|
|
end
|
|
|
|
function TypedSection.prepare(self)
|
|
AbstractSection.prepare(self)
|
|
|
|
local i, s
|
|
for i, s in ipairs(self:cfgsections()) do
|
|
AbstractSection.parse_optionals(self, s, true)
|
|
end
|
|
end
|
|
|
|
-- Return all matching UCI sections for this TypedSection
|
|
function TypedSection.cfgsections(self)
|
|
local sections = {}
|
|
self.map.uci:foreach(self.map.config, self.sectiontype,
|
|
function (section)
|
|
if self:checkscope(section[".name"]) then
|
|
table.insert(sections, section[".name"])
|
|
end
|
|
end)
|
|
|
|
return sections
|
|
end
|
|
|
|
-- Limits scope to sections that have certain option => value pairs
|
|
function TypedSection.depends(self, option, value)
|
|
table.insert(self.deps, {option=option, value=value})
|
|
end
|
|
|
|
function TypedSection.parse(self, novld)
|
|
if self.addremove then
|
|
-- Remove
|
|
local crval = REMOVE_PREFIX .. self.config
|
|
local name = self.map:formvaluetable(crval)
|
|
for k,v in pairs(name) do
|
|
if k:sub(-2) == ".x" then
|
|
k = k:sub(1, #k - 2)
|
|
end
|
|
if self:cfgvalue(k) and self:checkscope(k) then
|
|
self:remove(k)
|
|
end
|
|
end
|
|
end
|
|
|
|
local co
|
|
for i, k in ipairs(self:cfgsections()) do
|
|
AbstractSection.parse_dynamic(self, k)
|
|
if self.map:submitstate() then
|
|
Node.parse(self, k, novld)
|
|
end
|
|
AbstractSection.parse_optionals(self, k)
|
|
end
|
|
|
|
if self.addremove then
|
|
-- Create
|
|
local created
|
|
local crval = CREATE_PREFIX .. self.config .. "." .. self.sectiontype
|
|
local origin, name = next(self.map:formvaluetable(crval))
|
|
if self.anonymous then
|
|
if name then
|
|
created = self:create(nil, origin)
|
|
end
|
|
else
|
|
if name then
|
|
-- Ignore if it already exists
|
|
if self:cfgvalue(name) then
|
|
name = nil
|
|
self.err_invalid = true
|
|
else
|
|
name = self:checkscope(name)
|
|
|
|
if not name then
|
|
self.err_invalid = true
|
|
end
|
|
|
|
if name and #name > 0 then
|
|
created = self:create(name, origin) and name
|
|
if not created then
|
|
self.invalid_cts = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if created then
|
|
AbstractSection.parse_optionals(self, created)
|
|
end
|
|
end
|
|
|
|
if self.sortable then
|
|
local stval = RESORT_PREFIX .. self.config .. "." .. self.sectiontype
|
|
local order = self.map:formvalue(stval)
|
|
if order and #order > 0 then
|
|
local sids, sid = { }, nil
|
|
for sid in util.imatch(order) do
|
|
sids[#sids+1] = sid
|
|
end
|
|
if #sids > 0 then
|
|
self.map.uci:reorder(self.config, sids)
|
|
self.changed = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if created or self.changed then
|
|
self:push_events()
|
|
end
|
|
end
|
|
|
|
-- Verifies scope of sections
|
|
function TypedSection.checkscope(self, section)
|
|
-- Check if we are not excluded
|
|
if self.filter and not self:filter(section) then
|
|
return nil
|
|
end
|
|
|
|
-- Check if at least one dependency is met
|
|
if #self.deps > 0 and self:cfgvalue(section) then
|
|
local stat = false
|
|
|
|
for k, v in ipairs(self.deps) do
|
|
if self:cfgvalue(section)[v.option] == v.value then
|
|
stat = true
|
|
end
|
|
end
|
|
|
|
if not stat then
|
|
return nil
|
|
end
|
|
end
|
|
|
|
return self:validate(section)
|
|
end
|
|
|
|
|
|
-- Dummy validate function
|
|
function TypedSection.validate(self, section)
|
|
return section
|
|
end
|
|
|
|
|
|
--[[
|
|
AbstractValue - An abstract Value Type
|
|
null: Value can be empty
|
|
valid: A function returning the value if it is valid otherwise nil
|
|
depends: A table of option => value pairs of which one must be true
|
|
default: The default value
|
|
size: The size of the input fields
|
|
rmempty: Unset value if empty
|
|
optional: This value is optional (see AbstractSection.optionals)
|
|
]]--
|
|
AbstractValue = class(Node)
|
|
|
|
function AbstractValue.__init__(self, map, section, option, ...)
|
|
Node.__init__(self, ...)
|
|
self.section = section
|
|
self.option = option
|
|
self.map = map
|
|
self.config = map.config
|
|
self.tag_invalid = {}
|
|
self.tag_missing = {}
|
|
self.tag_reqerror = {}
|
|
self.tag_error = {}
|
|
self.deps = {}
|
|
--self.cast = "string"
|
|
|
|
self.track_missing = false
|
|
self.rmempty = true
|
|
self.default = nil
|
|
self.size = nil
|
|
self.optional = false
|
|
end
|
|
|
|
function AbstractValue.prepare(self)
|
|
self.cast = self.cast or "string"
|
|
end
|
|
|
|
-- Add a dependencie to another section field
|
|
function AbstractValue.depends(self, field, value)
|
|
local deps
|
|
if type(field) == "string" then
|
|
deps = {}
|
|
deps[field] = value
|
|
else
|
|
deps = field
|
|
end
|
|
|
|
table.insert(self.deps, deps)
|
|
end
|
|
|
|
-- Serialize dependencies
|
|
function AbstractValue.deplist2json(self, section, deplist)
|
|
local deps, i, d = { }
|
|
|
|
if type(self.deps) == "table" then
|
|
for i, d in ipairs(deplist or self.deps) do
|
|
local a, k, v = { }
|
|
for k, v in pairs(d) do
|
|
if k:find("!", 1, true) then
|
|
a[k] = v
|
|
elseif k:find(".", 1, true) then
|
|
a['cbid.%s' % k] = v
|
|
else
|
|
a['cbid.%s.%s.%s' %{ self.config, section, k }] = v
|
|
end
|
|
end
|
|
deps[#deps+1] = a
|
|
end
|
|
end
|
|
|
|
return util.serialize_json(deps)
|
|
end
|
|
|
|
-- Serialize choices
|
|
function AbstractValue.choices(self)
|
|
if type(self.keylist) == "table" and #self.keylist > 0 then
|
|
local i, k, v = nil, nil, {}
|
|
for i, k in ipairs(self.keylist) do
|
|
v[k] = self.vallist[i] or k
|
|
end
|
|
return v
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Generates the unique CBID
|
|
function AbstractValue.cbid(self, section)
|
|
return "cbid."..self.map.config.."."..section.."."..self.option
|
|
end
|
|
|
|
-- Return whether this object should be created
|
|
function AbstractValue.formcreated(self, section)
|
|
local key = "cbi.opt."..self.config.."."..section
|
|
return (self.map:formvalue(key) == self.option)
|
|
end
|
|
|
|
-- Returns the formvalue for this object
|
|
function AbstractValue.formvalue(self, section)
|
|
return self.map:formvalue(self:cbid(section))
|
|
end
|
|
|
|
function AbstractValue.additional(self, value)
|
|
self.optional = value
|
|
end
|
|
|
|
function AbstractValue.mandatory(self, value)
|
|
self.rmempty = not value
|
|
end
|
|
|
|
function AbstractValue.add_error(self, section, type, msg)
|
|
self.error = self.error or { }
|
|
self.error[section] = msg or type
|
|
|
|
self.section.error = self.section.error or { }
|
|
self.section.error[section] = self.section.error[section] or { }
|
|
table.insert(self.section.error[section], msg or type)
|
|
|
|
if type == "invalid" then
|
|
self.tag_invalid[section] = true
|
|
elseif type == "missing" then
|
|
self.tag_missing[section] = true
|
|
end
|
|
|
|
self.tag_error[section] = true
|
|
self.map.save = false
|
|
end
|
|
|
|
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
|
|
local val_err
|
|
fvalue, val_err = self:validate(fvalue, section)
|
|
fvalue = self:transform(fvalue)
|
|
|
|
if not fvalue and not novld then
|
|
self:add_error(section, "invalid", val_err)
|
|
end
|
|
|
|
if self.alias then
|
|
self.section.aliased = self.section.aliased or {}
|
|
self.section.aliased[section] = self.section.aliased[section] or {}
|
|
self.section.aliased[section][self.alias] = true
|
|
end
|
|
|
|
if fvalue and (self.forcewrite or not (fvalue == cvalue)) then
|
|
if self:write(section, fvalue) then
|
|
-- Push events
|
|
self.section.changed = true
|
|
--luci.util.append(self.map.events, self.events)
|
|
end
|
|
end
|
|
else -- Unset the UCI or error
|
|
if self.rmempty or self.optional then
|
|
if not self.alias or
|
|
not self.section.aliased or
|
|
not self.section.aliased[section] or
|
|
not self.section.aliased[section][self.alias]
|
|
then
|
|
if self:remove(section) then
|
|
-- Push events
|
|
self.section.changed = true
|
|
--luci.util.append(self.map.events, self.events)
|
|
end
|
|
end
|
|
elseif cvalue ~= fvalue and not novld then
|
|
-- trigger validator with nil value to get custom user error msg.
|
|
local _, val_err = self:validate(nil, section)
|
|
self:add_error(section, "missing", val_err)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Render if this value exists or if it is mandatory
|
|
function AbstractValue.render(self, s, scope)
|
|
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)
|
|
Node.render(self, scope)
|
|
end
|
|
end
|
|
|
|
-- Return the UCI value of this object
|
|
function AbstractValue.cfgvalue(self, section)
|
|
local value
|
|
if self.tag_error[section] then
|
|
value = self:formvalue(section)
|
|
else
|
|
value = self.map:get(section, self.alias or self.option)
|
|
end
|
|
|
|
if not value then
|
|
return nil
|
|
elseif not self.cast or self.cast == type(value) then
|
|
return value
|
|
elseif self.cast == "string" then
|
|
if type(value) == "table" then
|
|
return value[1]
|
|
end
|
|
elseif self.cast == "table" then
|
|
return { value }
|
|
end
|
|
end
|
|
|
|
-- Validate the form value
|
|
function AbstractValue.validate(self, value)
|
|
if self.datatype and value then
|
|
if type(value) == "table" then
|
|
local v
|
|
for _, v in ipairs(value) do
|
|
if v and #v > 0 and not verify_datatype(self.datatype, v) then
|
|
return nil
|
|
end
|
|
end
|
|
else
|
|
if not verify_datatype(self.datatype, value) then
|
|
return nil
|
|
end
|
|
end
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
AbstractValue.transform = AbstractValue.validate
|
|
|
|
|
|
-- Write to UCI
|
|
function AbstractValue.write(self, section, value)
|
|
return self.map:set(section, self.alias or self.option, value)
|
|
end
|
|
|
|
-- Remove from UCI
|
|
function AbstractValue.remove(self, section)
|
|
return self.map:del(section, self.alias or self.option)
|
|
end
|
|
|
|
|
|
|
|
|
|
--[[
|
|
Value - A one-line value
|
|
maxlength: The maximum length
|
|
]]--
|
|
Value = class(AbstractValue)
|
|
|
|
function Value.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/value"
|
|
self.keylist = {}
|
|
self.vallist = {}
|
|
self.readonly = nil
|
|
end
|
|
|
|
function Value.reset_values(self)
|
|
self.keylist = {}
|
|
self.vallist = {}
|
|
end
|
|
|
|
function Value.value(self, key, val)
|
|
val = val or key
|
|
table.insert(self.keylist, tostring(key))
|
|
table.insert(self.vallist, tostring(val))
|
|
end
|
|
|
|
function Value.parse(self, section, novld)
|
|
if self.readonly then return end
|
|
AbstractValue.parse(self, section, novld)
|
|
end
|
|
|
|
-- DummyValue - This does nothing except being there
|
|
DummyValue = class(AbstractValue)
|
|
|
|
function DummyValue.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/dvalue"
|
|
self.value = nil
|
|
end
|
|
|
|
function DummyValue.cfgvalue(self, section)
|
|
local value
|
|
if self.value then
|
|
if type(self.value) == "function" then
|
|
value = self:value(section)
|
|
else
|
|
value = self.value
|
|
end
|
|
else
|
|
value = AbstractValue.cfgvalue(self, section)
|
|
end
|
|
return value
|
|
end
|
|
|
|
function DummyValue.parse(self)
|
|
|
|
end
|
|
|
|
|
|
--[[
|
|
Flag - A flag being enabled or disabled
|
|
]]--
|
|
Flag = class(AbstractValue)
|
|
|
|
function Flag.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/fvalue"
|
|
|
|
self.enabled = "1"
|
|
self.disabled = "0"
|
|
self.default = self.disabled
|
|
end
|
|
|
|
-- A flag can only have two states: set or unset
|
|
function Flag.parse(self, section, novld)
|
|
local fexists = self.map:formvalue(
|
|
FEXIST_PREFIX .. self.config .. "." .. section .. "." .. self.option)
|
|
|
|
if fexists then
|
|
local fvalue = self:formvalue(section) and self.enabled or self.disabled
|
|
local cvalue = self:cfgvalue(section)
|
|
local val_err
|
|
fvalue, val_err = self:validate(fvalue, section)
|
|
if not fvalue then
|
|
if not novld then
|
|
self:add_error(section, "invalid", val_err)
|
|
end
|
|
return
|
|
end
|
|
if fvalue == self.default and (self.optional or self.rmempty) then
|
|
self:remove(section)
|
|
else
|
|
self:write(section, fvalue)
|
|
end
|
|
if (fvalue ~= cvalue) then self.section.changed = true end
|
|
else
|
|
self:remove(section)
|
|
self.section.changed = true
|
|
end
|
|
end
|
|
|
|
function Flag.cfgvalue(self, section)
|
|
return AbstractValue.cfgvalue(self, section) or self.default
|
|
end
|
|
function Flag.validate(self, value)
|
|
return value
|
|
end
|
|
|
|
--[[
|
|
ListValue - A one-line value predefined in a list
|
|
widget: The widget that will be used (select, radio)
|
|
]]--
|
|
ListValue = class(AbstractValue)
|
|
|
|
function ListValue.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/lvalue"
|
|
|
|
self.size = 1
|
|
self.widget = "select"
|
|
|
|
self:reset_values()
|
|
end
|
|
|
|
function ListValue.reset_values(self)
|
|
self.keylist = {}
|
|
self.vallist = {}
|
|
self.deplist = {}
|
|
end
|
|
|
|
function ListValue.value(self, key, val, ...)
|
|
if luci.util.contains(self.keylist, key) then
|
|
return
|
|
end
|
|
|
|
val = val or key
|
|
table.insert(self.keylist, tostring(key))
|
|
table.insert(self.vallist, tostring(val))
|
|
table.insert(self.deplist, {...})
|
|
end
|
|
|
|
function ListValue.validate(self, val)
|
|
if luci.util.contains(self.keylist, val) then
|
|
return val
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
MultiValue - Multiple delimited values
|
|
widget: The widget that will be used (select, checkbox)
|
|
delimiter: The delimiter that will separate the values (default: " ")
|
|
]]--
|
|
MultiValue = class(AbstractValue)
|
|
|
|
function MultiValue.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/mvalue"
|
|
|
|
self.widget = "checkbox"
|
|
self.delimiter = " "
|
|
|
|
self:reset_values()
|
|
end
|
|
|
|
function MultiValue.render(self, ...)
|
|
if self.widget == "select" and not self.size then
|
|
self.size = #self.vallist
|
|
end
|
|
|
|
AbstractValue.render(self, ...)
|
|
end
|
|
|
|
function MultiValue.reset_values(self)
|
|
self.keylist = {}
|
|
self.vallist = {}
|
|
self.deplist = {}
|
|
end
|
|
|
|
function MultiValue.value(self, key, val)
|
|
if luci.util.contains(self.keylist, key) then
|
|
return
|
|
end
|
|
|
|
val = val or key
|
|
table.insert(self.keylist, tostring(key))
|
|
table.insert(self.vallist, tostring(val))
|
|
end
|
|
|
|
function MultiValue.valuelist(self, section)
|
|
local val = self:cfgvalue(section)
|
|
|
|
if not(type(val) == "string") then
|
|
return {}
|
|
end
|
|
|
|
return luci.util.split(val, self.delimiter)
|
|
end
|
|
|
|
function MultiValue.validate(self, val)
|
|
val = (type(val) == "table") and val or {val}
|
|
|
|
local result
|
|
|
|
for i, value in ipairs(val) do
|
|
if luci.util.contains(self.keylist, value) then
|
|
result = result and (result .. self.delimiter .. value) or value
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
StaticList = class(MultiValue)
|
|
|
|
function StaticList.__init__(self, ...)
|
|
MultiValue.__init__(self, ...)
|
|
self.cast = "table"
|
|
self.valuelist = self.cfgvalue
|
|
|
|
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.values and not self.override_values then
|
|
for k, v in pairs(vs.values) do
|
|
self:value(k, v)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function StaticList.validate(self, value)
|
|
value = (type(value) == "table") and value or {value}
|
|
|
|
local valid = {}
|
|
for i, v in ipairs(value) do
|
|
if luci.util.contains(self.keylist, v) then
|
|
table.insert(valid, v)
|
|
end
|
|
end
|
|
return valid
|
|
end
|
|
|
|
|
|
DynamicList = class(AbstractValue)
|
|
|
|
function DynamicList.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/dynlist"
|
|
self.cast = "table"
|
|
self:reset_values()
|
|
end
|
|
|
|
function DynamicList.reset_values(self)
|
|
self.keylist = {}
|
|
self.vallist = {}
|
|
end
|
|
|
|
function DynamicList.value(self, key, val)
|
|
val = val or key
|
|
table.insert(self.keylist, tostring(key))
|
|
table.insert(self.vallist, tostring(val))
|
|
end
|
|
|
|
function DynamicList.write(self, section, value)
|
|
local t = { }
|
|
|
|
if type(value) == "table" then
|
|
local x
|
|
for _, x in ipairs(value) do
|
|
if x and #x > 0 then
|
|
t[#t+1] = x
|
|
end
|
|
end
|
|
else
|
|
t = { value }
|
|
end
|
|
|
|
if self.cast == "string" then
|
|
value = table.concat(t, " ")
|
|
else
|
|
value = t
|
|
end
|
|
|
|
return AbstractValue.write(self, section, value)
|
|
end
|
|
|
|
function DynamicList.cfgvalue(self, section)
|
|
local value = AbstractValue.cfgvalue(self, section)
|
|
|
|
if type(value) == "string" then
|
|
local x
|
|
local t = { }
|
|
for x in value:gmatch("%S+") do
|
|
if #x > 0 then
|
|
t[#t+1] = x
|
|
end
|
|
end
|
|
value = t
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
function DynamicList.formvalue(self, section)
|
|
local value = AbstractValue.formvalue(self, section)
|
|
|
|
if type(value) == "string" then
|
|
if self.cast == "string" then
|
|
local x
|
|
local t = { }
|
|
for x in value:gmatch("%S+") do
|
|
t[#t+1] = x
|
|
end
|
|
value = t
|
|
else
|
|
value = { value }
|
|
end
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
|
|
DropDown = class(MultiValue)
|
|
|
|
function DropDown.__init__(self, ...)
|
|
ListValue.__init__(self, ...)
|
|
self.template = "cbi/dropdown"
|
|
self.delimiter = " "
|
|
end
|
|
|
|
|
|
--[[
|
|
TextValue - A multi-line value
|
|
rows: Rows
|
|
]]--
|
|
TextValue = class(AbstractValue)
|
|
|
|
function TextValue.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/tvalue"
|
|
end
|
|
|
|
--[[
|
|
Button
|
|
]]--
|
|
Button = class(AbstractValue)
|
|
|
|
function Button.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/button"
|
|
self.inputstyle = nil
|
|
self.rmempty = true
|
|
self.unsafeupload = false
|
|
end
|
|
|
|
|
|
FileUpload = class(AbstractValue)
|
|
|
|
function FileUpload.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/upload"
|
|
if not self.map.upload_fields then
|
|
self.map.upload_fields = { self }
|
|
else
|
|
self.map.upload_fields[#self.map.upload_fields+1] = self
|
|
end
|
|
end
|
|
|
|
function FileUpload.formcreated(self, section)
|
|
if self.unsafeupload then
|
|
return AbstractValue.formcreated(self, section) or
|
|
self.map:formvalue("cbi.rlf."..section.."."..self.option) or
|
|
self.map:formvalue("cbi.rlf."..section.."."..self.option..".x") or
|
|
self.map:formvalue("cbid."..self.map.config.."."..section.."."..self.option..".textbox")
|
|
else
|
|
return AbstractValue.formcreated(self, section) or
|
|
self.map:formvalue("cbid."..self.map.config.."."..section.."."..self.option..".textbox")
|
|
end
|
|
end
|
|
|
|
function FileUpload.cfgvalue(self, section)
|
|
local val = AbstractValue.cfgvalue(self, section)
|
|
if val and fs.access(val) then
|
|
return val
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- If we have a new value, use it
|
|
-- otherwise use old value
|
|
-- deletion should be managed by a separate button object
|
|
-- unless self.unsafeupload is set in which case if the user
|
|
-- choose to remove the old file we do so.
|
|
-- Also, allow to specify (via textbox) a file already on router
|
|
function FileUpload.formvalue(self, section)
|
|
local val = AbstractValue.formvalue(self, section)
|
|
if val then
|
|
if self.unsafeupload then
|
|
if not self.map:formvalue("cbi.rlf."..section.."."..self.option) and
|
|
not self.map:formvalue("cbi.rlf."..section.."."..self.option..".x")
|
|
then
|
|
return val
|
|
end
|
|
fs.unlink(val)
|
|
self.value = nil
|
|
return nil
|
|
elseif val ~= "" then
|
|
return val
|
|
end
|
|
end
|
|
val = luci.http.formvalue("cbid."..self.map.config.."."..section.."."..self.option..".textbox")
|
|
if val == "" then
|
|
val = nil
|
|
end
|
|
if not self.unsafeupload then
|
|
if not val then
|
|
val = self.map:formvalue("cbi.rlf."..section.."."..self.option)
|
|
end
|
|
end
|
|
return val
|
|
end
|
|
|
|
function FileUpload.remove(self, section)
|
|
if self.unsafeupload then
|
|
local val = AbstractValue.formvalue(self, section)
|
|
if val and fs.access(val) then fs.unlink(val) end
|
|
return AbstractValue.remove(self, section)
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
FileBrowser = class(AbstractValue)
|
|
|
|
function FileBrowser.__init__(self, ...)
|
|
AbstractValue.__init__(self, ...)
|
|
self.template = "cbi/browser"
|
|
end
|