--[[ LuCI - Configuration Bind Interface Description: Offers an interface for binding configuration values to certain data types. Supports value and range validation and basic dependencies. FileId: $Id$ License: Copyright 2008 Steven Barth 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("luci.cbi", package.seeall) require("luci.template") require("luci.util") require("luci.http") require("luci.uvl") local uci = require("luci.model.uci") local class = luci.util.class local instanceof = luci.util.instanceof FORM_NODATA = 0 FORM_VALID = 1 FORM_INVALID = -1 AUTO = true CREATE_PREFIX = "cbi.cts." REMOVE_PREFIX = "cbi.rts." -- Loads a CBI map from given file, creating an environment and returns it function load(cbimap, ...) require("luci.fs") require("luci.i18n") require("luci.config") require("luci.util") local cbidir = luci.util.libpath() .. "/model/cbi/" local func, err = loadfile(cbidir..cbimap..".lua") if not func then return nil end luci.i18n.loadc("cbi") luci.util.resfenv(func) luci.util.updfenv(func, luci.cbi) luci.util.extfenv(func, "translate", luci.i18n.translate) luci.util.extfenv(func, "translatef", luci.i18n.translatef) luci.util.extfenv(func, "arg", {...}) local maps = {func()} for i, map in ipairs(maps) do if not instanceof(map, Node) then error("CBI map returns no valid map object!") return nil end end 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 stat, err = node.map.validator:validate_section(node.config, name, co) if err then node.map.save = false if err:is(luci.uvl.errors.ERR_DEPENDENCY) then node.tag_deperror[name] = true else node.tag_invalid[name] = true end for i, v in ipairs(err.childs) do if v.option and node.fields[v.option] then if v:is(luci.uvl.errors.ERR_DEPENDENCY) then node.fields[v.option].tag_reqerror[name] = true elseif v:is(luci.uvl.errors.ERR_OPT_REQUIRED) then node.fields[v.option].tag_missing[name] = true node.tag_deperror[name] = true elseif v:is(luci.uvl.errors.ERR_OPTION) then node.fields[v.option].tag_invalid[name] = true end end end 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() function Node.__init__(self, title, description) self.children = {} self.title = title or "" self.description = description or "" 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 self.title = title or luci.i18n.translate( key, option or section or config ) self.description = description or luci.i18n.translate( key .. "_desc", "" ) 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, ...) for k, node in ipairs(self.children) do 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 --[[ Map - A map describing a configuration file ]]-- Map = class(Node) function Map.__init__(self, config, ...) Node.__init__(self, ...) Node._i18n(self, config, nil, nil, ...) self.config = config self.parsechain = {self.config} self.template = "cbi/map" self.uci = uci.cursor() self.save = true 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.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 -- Chain foreign config function Map.chain(self, config) table.insert(self.parsechain, config) end -- Use optimized UCI writing function Map.parse(self, ...) Node.parse(self, ...) if self.save then for i, config in ipairs(self.parsechain) do self.uci:save(config) end if luci.http.formvalue("cbi.apply") then for i, config in ipairs(self.parsechain) do self.uci:commit(config) self.uci:apply(config) -- Refresh data because commit changes section names self.uci:load(config) end -- Reparse sections Node.parse(self, ...) end for i, config in ipairs(self.parsechain) do self.uci:unload(config) end end 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 option then return self.uci:set(self.config, section, option, value) else return self.uci:set(self.config, section, value) 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 --[[ 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 end function SimpleForm.parse(self, ...) if luci.http.formvalue("cbi.submit") 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]) end end local state = not luci.http.formvalue("cbi.submit") and 0 or valid and 1 or -1 self.dorender = not self.handle or self:handle(state, self.data) ~= false end function SimpleForm.render(self, ...) if self.dorender then Node.render(self, ...) end 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 --[[ 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.optional = true self.addremove = false self.dynamic = false 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 elseif class == true then error("No valid class was given and autodetection failed.") else error("class must be a descendant 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 = luci.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 = luci.util.clone(self:cfgvalue(section)) local form = luci.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) local stat if section then stat = 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 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 = {} datasource.config = "table" self.data = data function datasource.get(self, section, option) return data[section] and data[section][option] 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) for i, k in ipairs(self:cfgsections()) do if luci.http.formvalue("cbi.submit") 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 --[[ NamedSection - A fixed configuration section defined by its name ]]-- 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 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 luci.http.formvalue("cbi.rns."..path) and self:remove(s) then return end else -- Create and apply default values if luci.http.formvalue("cbi.cns."..path) then self:create(s) return end end end if active then AbstractSection.parse_dynamic(self, s) if luci.http.formvalue("cbi.submit") then Node.parse(self, s) if not self.override_scheme and self.map.scheme then _uvl_validate_section(self, s) end end AbstractSection.parse_optionals(self, s) 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, ...) Node._i18n(self, map.config, type, nil, ...) 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 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) if self.addremove then -- Remove local crval = REMOVE_PREFIX .. self.config local name = luci.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 local co for i, k in ipairs(self:cfgsections()) do AbstractSection.parse_dynamic(self, k) if luci.http.formvalue("cbi.submit") then Node.parse(self, k) if not self.override_scheme and self.map.scheme then _uvl_validate_section(self, k) end end AbstractSection.parse_optionals(self, k) end if self.addremove then -- Create local created local crval = CREATE_PREFIX .. self.config .. "." .. self.sectiontype local name = luci.http.formvalue(crval) if self.anonymous then if name then created = self:create() end else if name then -- 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 if name and #name > 0 then created = self:create(name) and name end end end if created then AbstractSection.parse_optionals(self, created) end 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 = false self.default = nil self.size = nil self.optional = false -- 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) self.rmempty = not vs.required self.cast = (vs.type == "list") and "list" or "string" self.title = self.title or vs.title self.description = self.description or vs.descr 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 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=deps, add=""}) 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 (luci.http.formvalue(key) == self.option) end -- Returns the formvalue for this object function AbstractValue.formvalue(self, section) return luci.http.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.parse(self, section) local fvalue = self:formvalue(section) local cvalue = self:cfgvalue(section) if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI fvalue = self:transform(self:validate(fvalue, section)) if not fvalue then self.tag_invalid[section] = true end if fvalue and not (fvalue == cvalue) then self:write(section, fvalue) end else -- Unset the UCI or error if self.rmempty or self.optional then self:remove(section) elseif self.track_missing and (not fvalue or fvalue ~= cvalue) then self.tag_missing[section] = true 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:cfgvalue(s) or self:formcreated(s) then scope = scope or {} scope.section = s scope.cbid = self:cbid(s) scope.striptags = luci.util.striptags scope.ifattr = function(cond,key,val) if cond then return string.format( ' %s="%s"', tostring(key), luci.util.pcdata(tostring( val or scope[key] or (type(self[key]) ~= "function" and self[key]) or "" )) ) else return '' end end scope.attr = function(...) return scope.ifattr( true, ... ) end Node.render(self, scope) end end -- Return the UCI value of this object function AbstractValue.cfgvalue(self, section) local value = self.map:get(section, self.option) if 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) return value end AbstractValue.transform = AbstractValue.validate -- 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) 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 = {} end function Value.value(self, key, val) val = val or key table.insert(self.keylist, tostring(key)) table.insert(self.vallist, tostring(val)) 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.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" 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 --[[ 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.keylist = {} self.vallist = {} self.size = 1 self.widget = "select" 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 if self.rmempty or self.optional then self:value("") end for k, v in pairs(vs.values) do local deps = {} if not self.override_dependencies and vs.enum_depends and vs.enum_depends[k] then for i, dep in ipairs(vs.enum_depends[k]) do table.insert(deps, _uvl_strip_remote_dependencies(dep)) end end self:value(k, v, unpack(deps)) end end end 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)) for i, deps in ipairs({...}) do table.insert(self.deps, {add = "-"..key, deps=deps}) end 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.keylist = {} self.vallist = {} self.widget = "checkbox" self.delimiter = " " 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.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.valuelist, 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.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.validate(self, value, section) value = (type(value) == "table") and value or {value} local valid = {} for i, v in ipairs(value) do if v and #v > 0 and not luci.http.formvalue("cbi.rle."..section.."."..self.option.."."..i) then table.insert(valid, v) end end return valid 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 end