luci/core/src/ffluci/cbi.lua

746 lines
16 KiB
Lua
Raw Normal View History

--[[
FFLuCI - Configuration Bind Interface
Description:
Offers an interface for binding confiugration values to certain
data types. Supports value and range validation and basic dependencies.
FileId:
$Id$
License:
Copyright 2008 Steven Barth <steven@midlink.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--
module("ffluci.cbi", package.seeall)
require("ffluci.template")
require("ffluci.util")
2008-03-20 20:33:43 +00:00
require("ffluci.http")
require("ffluci.model.uci")
local class = ffluci.util.class
local instanceof = ffluci.util.instanceof
-- Loads a CBI map from given file, creating an environment and returns it
function load(cbimap)
require("ffluci.fs")
require("ffluci.i18n")
require("ffluci.config")
local cbidir = ffluci.config.path .. "/model/cbi/"
local func, err = loadfile(cbidir..cbimap..".lua")
if not func then
return nil
end
ffluci.i18n.loadc("cbi")
ffluci.util.resfenv(func)
ffluci.util.updfenv(func, ffluci.cbi)
ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
local map = func()
if not instanceof(map, Map) then
error("CBI map returns no valid map object!")
return nil
end
return map
end
-- Node pseudo abstract class
2008-03-16 20:13:11 +00:00
Node = class()
function Node.__init__(self, title, description)
self.children = {}
self.title = title or ""
self.description = description or ""
self.template = "cbi/node"
end
2008-03-16 20:13:11 +00:00
-- Append child nodes
function Node.append(self, obj)
2008-03-16 20:13:11 +00:00
table.insert(self.children, obj)
end
-- Parse this node and its children
function Node.parse(self, ...)
2008-03-20 20:33:43 +00:00
for k, child in ipairs(self.children) do
child:parse(...)
2008-03-20 20:33:43 +00:00
end
end
-- Render this node
2008-03-18 22:08:46 +00:00
function Node.render(self)
ffluci.template.render(self.template, {self=self})
2008-03-18 22:08:46 +00:00
end
-- Render the children
function Node.render_children(self, ...)
for k, node in ipairs(self.children) do
node:render(...)
end
end
2008-04-14 10:58:34 +00:00
--[[
A simple template element
]]--
Template = class(Node)
function Template.__init__(self, template)
Node.__init__(self)
self.template = template
end
--[[
Map - A map describing a configuration file
]]--
2008-03-16 20:13:11 +00:00
Map = class(Node)
function Map.__init__(self, config, ...)
Node.__init__(self, ...)
self.config = config
self.template = "cbi/map"
self.uci = ffluci.model.uci.Session()
self.ucidata = self.uci:sections(self.config)
2008-03-22 21:26:44 +00:00
if not self.ucidata then
error("Unable to read UCI data: " .. self.config)
end
2008-03-22 21:26:44 +00:00
end
-- Creates a child section
function Map.section(self, class, ...)
2008-03-18 22:08:46 +00:00
if instanceof(class, AbstractSection) then
local obj = class(self, ...)
2008-03-20 20:33:43 +00:00
self:append(obj)
return obj
else
error("class must be a descendent of AbstractSection")
end
end
-- UCI add
function Map.add(self, sectiontype)
local name = self.uci:add(self.config, sectiontype)
if name then
self.ucidata[name] = {}
self.ucidata[name][".type"] = sectiontype
self.ucidata[".order"] = self.ucidata[".order"] or {}
table.insert(self.ucidata[".order"], name)
end
return name
end
-- UCI set
function Map.set(self, section, option, value)
local stat = self.uci:set(self.config, section, option, value)
if stat then
local val = self.uci:get(self.config, section, option)
if option then
self.ucidata[section][option] = val
else
if not self.ucidata[section] then
self.ucidata[section] = {}
end
self.ucidata[section][".type"] = val
self.ucidata[".order"] = self.ucidata[".order"] or {}
table.insert(self.ucidata[".order"], section)
end
end
return stat
end
-- UCI del
function Map.del(self, section, option)
local stat = self.uci:del(self.config, section, option)
if stat then
if option then
self.ucidata[section][option] = nil
else
self.ucidata[section] = nil
for i, k in ipairs(self.ucidata[".order"]) do
if section == k then
table.remove(self.ucidata[".order"], i)
end
end
end
end
return stat
end
-- UCI get (cached)
function Map.get(self, section, option)
if not section then
return self.ucidata
elseif option and self.ucidata[section] then
return self.ucidata[section][option]
else
return self.ucidata[section]
end
end
--[[
AbstractSection
]]--
AbstractSection = class(Node)
function AbstractSection.__init__(self, map, sectiontype, ...)
Node.__init__(self, ...)
2008-03-18 22:08:46 +00:00
self.sectiontype = sectiontype
self.map = map
self.config = map.config
self.optionals = {}
self.optional = true
self.addremove = false
self.dynamic = false
end
-- Appends a new option
function AbstractSection.option(self, class, ...)
if instanceof(class, AbstractValue) then
local obj = class(self.map, ...)
2008-03-20 20:33:43 +00:00
self:append(obj)
return obj
else
error("class must be a descendent of AbstractValue")
end
end
-- Parse optional options
function AbstractSection.parse_optionals(self, section)
if not self.optional then
return
end
self.optionals[section] = {}
local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
for k,v in ipairs(self.children) do
if v.optional and not v:cfgvalue(section) then
if field == v.option then
field = nil
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 = ffluci.util.clone(self:cfgvalue(section))
local form = ffluci.http.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:add_dynamic(key, true)
end
end
end
-- Returns the section's UCI table
function AbstractSection.cfgvalue(self, section)
return self.map:get(section)
end
-- Removes the section
function AbstractSection.remove(self, section)
return self.map:del(section)
end
-- Creates the section
function AbstractSection.create(self, section)
return self.map:set(section, nil, self.sectiontype)
end
--[[
NamedSection - A fixed configuration section defined by its name
]]--
NamedSection = class(AbstractSection)
function NamedSection.__init__(self, map, section, ...)
AbstractSection.__init__(self, map, ...)
self.template = "cbi/nsection"
2008-03-18 22:08:46 +00:00
self.section = section
self.addremove = false
end
function NamedSection.parse(self)
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 ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
return
end
else -- Create and apply default values
if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
for k,v in pairs(self.children) do
v:write(s, v.default)
end
end
end
end
if active then
AbstractSection.parse_dynamic(self, s)
if ffluci.http.formvalue("cbi.submit") then
Node.parse(self, s)
end
AbstractSection.parse_optionals(self, s)
end
2008-03-18 22:08:46 +00:00
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
2008-03-27 23:14:01 +00:00
validate: a validation function returning nil if the section is invalid
]]--
TypedSection = class(AbstractSection)
2008-03-18 22:08:46 +00:00
function TypedSection.__init__(self, ...)
AbstractSection.__init__(self, ...)
self.template = "cbi/tsection"
2008-03-27 23:14:01 +00:00
self.deps = {}
self.excludes = {}
self.anonymous = false
2008-03-27 23:14:01 +00:00
end
-- Return all matching UCI sections for this TypedSection
function TypedSection.cfgsections(self)
local sections = {}
local map = self.map:get()
if not map[".order"] then
return sections
end
for i, k in pairs(map[".order"]) do
if map[k][".type"] == self.sectiontype then
2008-03-27 23:14:01 +00:00
if self:checkscope(k) then
table.insert(sections, k)
2008-03-27 23:14:01 +00:00
end
end
end
return sections
end
-- Creates a new section of this type with the given name (or anonymous)
function TypedSection.create(self, name)
if name then
self.map:set(name, nil, self.sectiontype)
else
name = self.map:add(self.sectiontype)
end
for k,v in pairs(self.children) do
if v.default then
self.map:set(name, v.option, v.default)
end
end
end
2008-03-27 23:14:01 +00:00
-- 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
-- Excludes several sections by name
function TypedSection.exclude(self, field)
self.excludes[field] = true
end
2008-03-22 21:26:44 +00:00
function TypedSection.parse(self)
if self.addremove then
-- Create
local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
local name = ffluci.http.formvalue(crval)
if self.anonymous then
if name then
self:create()
end
else
if name then
2008-03-27 23:14:01 +00:00
-- Ignore if it already exists
if self:cfgvalue(name) then
name = nil;
end
name = self:checkscope(name)
if not name then
self.err_invalid = true
end
2008-03-27 23:14:01 +00:00
if name and name:len() > 0 then
self:create(name)
end
end
end
-- Remove
crval = "cbi.rts." .. self.config
name = ffluci.http.formvaluetable(crval)
for k,v in pairs(name) do
if self:cfgvalue(k) and self:checkscope(k) then
self:remove(k)
end
end
end
for i, k in ipairs(self:cfgsections()) do
AbstractSection.parse_dynamic(self, k)
if ffluci.http.formvalue("cbi.submit") then
Node.parse(self, k)
end
AbstractSection.parse_optionals(self, k)
2008-03-22 21:26:44 +00:00
end
end
-- Render the children
function TypedSection.render_children(self, section)
for k, node in ipairs(self.children) do
node:render(section)
end
end
2008-03-27 23:14:01 +00:00
-- Verifies scope of sections
function TypedSection.checkscope(self, section)
-- Check if we are not excluded
if self.excludes[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
2008-03-27 23:14:01 +00:00
if not stat then
return nil
end
end
2008-03-27 23:14:01 +00:00
return self:validate(section)
end
2008-03-27 23:14:01 +00:00
-- Dummy validate function
function TypedSection.validate(self, section)
return section
end
--[[
AbstractValue - An abstract Value Type
null: Value can be empty
2008-03-20 20:33:43 +00:00
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
2008-03-18 22:08:46 +00:00
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, option, ...)
2008-03-16 20:13:11 +00:00
Node.__init__(self, ...)
self.option = option
self.map = map
self.config = map.config
self.tag_invalid = {}
2008-03-27 23:14:01 +00:00
self.deps = {}
2008-03-27 23:14:01 +00:00
self.rmempty = false
self.default = nil
self.size = nil
self.optional = false
2008-03-16 20:13:11 +00:00
end
2008-03-20 20:33:43 +00:00
2008-03-27 23:14:01 +00:00
-- Add a dependencie to another section field
function AbstractValue.depends(self, field, value)
table.insert(self.deps, {field=field, value=value})
end
-- Return whether this object should be created
function AbstractValue.formcreated(self, section)
local key = "cbi.opt."..self.config.."."..section
return (ffluci.http.formvalue(key) == self.option)
end
-- Returns the formvalue for this object
function AbstractValue.formvalue(self, section)
local key = "cbid."..self.map.config.."."..section.."."..self.option
2008-03-20 20:33:43 +00:00
return ffluci.http.formvalue(key)
end
function AbstractValue.parse(self, section)
local fvalue = self:formvalue(section)
2008-03-27 23:14:01 +00:00
if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
fvalue = self:validate(fvalue)
if not fvalue then
self.tag_invalid[section] = true
end
if fvalue and not (fvalue == self:cfgvalue(section)) then
self:write(section, fvalue)
end
else -- Unset the UCI or error
if self.rmempty or self.optional then
self:remove(section)
end
end
2008-03-22 21:26:44 +00:00
end
-- Render if this value exists or if it is mandatory
2008-03-27 23:14:01 +00:00
function AbstractValue.render(self, s)
if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
ffluci.template.render(self.template, {self=self, section=s})
end
2008-03-20 20:33:43 +00:00
end
-- Return the UCI value of this object
function AbstractValue.cfgvalue(self, section)
return self.map:get(section, self.option)
end
-- Validate the form value
2008-03-27 23:14:01 +00:00
function AbstractValue.validate(self, value)
return value
2008-03-20 20:33:43 +00:00
end
-- Write to UCI
function AbstractValue.write(self, section, value)
return self.map:set(section, self.option, value)
end
-- Remove from UCI
function AbstractValue.remove(self, section)
return self.map:del(section, self.option)
2008-03-20 20:33:43 +00:00
end
--[[
2008-03-18 22:08:46 +00:00
Value - A one-line value
maxlength: The maximum length
isnumber: The value must be a valid (floating point) number
isinteger: The value must be a valid integer
ispositive: The value must be positive (and a number)
]]--
Value = class(AbstractValue)
2008-03-16 20:13:11 +00:00
function Value.__init__(self, ...)
AbstractValue.__init__(self, ...)
self.template = "cbi/value"
self.maxlength = nil
self.isnumber = false
self.isinteger = false
end
-- This validation is a bit more complex
function Value.validate(self, val)
if self.maxlength and tostring(val):len() > self.maxlength then
val = nil
end
2008-03-27 23:14:01 +00:00
return ffluci.util.validate(val, self.isnumber, self.isinteger)
2008-03-16 20:13:11 +00:00
end
-- DummyValue - This does nothing except being there
DummyValue = class(AbstractValue)
function DummyValue.__init__(self, map, ...)
AbstractValue.__init__(self, map, ...)
self.template = "cbi/dvalue"
self.value = nil
end
function DummyValue.parse(self)
end
function DummyValue.render(self, s)
ffluci.template.render(self.template, {self=self, section=s})
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"
end
-- A flag can only have two states: set or unset
function Flag.parse(self, section)
local fvalue = self:formvalue(section)
if fvalue then
fvalue = self.enabled
else
fvalue = self.disabled
end
if fvalue == self.enabled or (not self.optional and not self.rmempty) then
if not(fvalue == self:cfgvalue(section)) then
self:write(section, fvalue)
end
else
self:remove(section)
end
end
--[[
2008-03-18 22:08:46 +00:00
ListValue - A one-line value predefined in a list
widget: The widget that will be used (select, radio)
]]--
2008-03-18 22:08:46 +00:00
ListValue = class(AbstractValue)
2008-03-18 22:08:46 +00:00
function ListValue.__init__(self, ...)
AbstractValue.__init__(self, ...)
2008-03-20 20:33:43 +00:00
self.template = "cbi/lvalue"
self.keylist = {}
self.vallist = {}
2008-03-18 22:08:46 +00:00
self.size = 1
self.widget = "select"
2008-03-18 22:08:46 +00:00
end
2008-03-27 23:14:01 +00:00
function ListValue.value(self, key, val)
2008-03-18 22:08:46 +00:00
val = val or key
table.insert(self.keylist, tostring(key))
table.insert(self.vallist, tostring(val))
end
function ListValue.validate(self, val)
if ffluci.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.keylist = {}
self.vallist = {}
self.widget = "checkbox"
self.delimiter = " "
end
2008-03-27 23:14:01 +00:00
function MultiValue.value(self, key, val)
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 ffluci.util.split(val, self.delimiter)
end
function MultiValue.validate(self, val)
if not(type(val) == "string") then
return nil
end
local result = ""
for value in val:gmatch("[^\n]+") do
if ffluci.util.contains(self.keylist, value) then
result = result .. self.delimiter .. value
end
end
if result:len() > 0 then
return result:sub(self.delimiter:len() + 1)
else
return nil
end
end