2 FFLuCI - Configuration Bind Interface
5 Offers an interface for binding confiugration values to certain
6 data types. Supports value and range validation and basic dependencies.
12 Copyright 2008 Steven Barth <steven@midlink.org>
14 Licensed under the Apache License, Version 2.0 (the "License");
15 you may not use this file except in compliance with the License.
16 You may obtain a copy of the License at
18 http://www.apache.org/licenses/LICENSE-2.0
20 Unless required by applicable law or agreed to in writing, software
21 distributed under the License is distributed on an "AS IS" BASIS,
22 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 See the License for the specific language governing permissions and
24 limitations under the License.
27 module("ffluci.cbi", package.seeall)
29 require("ffluci.template")
30 require("ffluci.util")
31 require("ffluci.http")
32 require("ffluci.model.uci")
34 local class = ffluci.util.class
35 local instanceof = ffluci.util.instanceof
37 -- Loads a CBI map from given file, creating an environment and returns it
40 require("ffluci.i18n")
41 require("ffluci.config")
43 local cbidir = ffluci.config.path .. "/model/cbi/"
44 local func, err = loadfile(cbidir..cbimap..".lua")
50 ffluci.i18n.loadc("cbi")
52 ffluci.util.resfenv(func)
53 ffluci.util.updfenv(func, ffluci.cbi)
54 ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
58 if not instanceof(map, Map) then
59 error("CBI map returns no valid map object!")
66 -- Node pseudo abstract class
69 function Node.__init__(self, title, description)
71 self.title = title or ""
72 self.description = description or ""
73 self.template = "cbi/node"
77 function Node.append(self, obj)
78 table.insert(self.children, obj)
81 -- Parse this node and its children
82 function Node.parse(self, ...)
83 for k, child in ipairs(self.children) do
89 function Node.render(self)
90 ffluci.template.render(self.template, {self=self})
93 -- Render the children
94 function Node.render_children(self, ...)
95 for k, node in ipairs(self.children) do
102 Map - A map describing a configuration file
106 function Map.__init__(self, config, ...)
107 Node.__init__(self, ...)
109 self.template = "cbi/map"
110 self.uci = ffluci.model.uci.Session()
111 self.ucidata = self.uci:show(self.config)
112 if not self.ucidata then
113 error("Unable to read UCI data: " .. self.config)
115 if not self.ucidata[self.config] then
116 self.ucidata[self.config] = {}
118 self.ucidata = self.ucidata[self.config]
122 -- Creates a child section
123 function Map.section(self, class, ...)
124 if instanceof(class, AbstractSection) then
125 local obj = class(self, ...)
129 error("class must be a descendent of AbstractSection")
134 function Map.add(self, sectiontype)
135 local name = self.uci:add(self.config, sectiontype)
137 self.ucidata[name] = {}
138 self.ucidata[name][".type"] = sectiontype
144 function Map.set(self, section, option, value)
145 local stat = self.uci:set(self.config, section, option, value)
147 local val = self.uci:get(self.config, section, option)
149 self.ucidata[section][option] = val
151 if not self.ucidata[section] then
152 self.ucidata[section] = {}
154 self.ucidata[section][".type"] = val
161 function Map.del(self, section, option)
162 local stat = self.uci:del(self.config, section, option)
165 self.ucidata[section][option] = nil
167 self.ucidata[section] = nil
174 function Map.get(self, section, option)
177 elseif option and self.ucidata[section] then
178 return self.ucidata[section][option]
180 return self.ucidata[section]
188 AbstractSection = class(Node)
190 function AbstractSection.__init__(self, map, sectiontype, ...)
191 Node.__init__(self, ...)
192 self.sectiontype = sectiontype
194 self.config = map.config
198 self.addremove = false
202 -- Appends a new option
203 function AbstractSection.option(self, class, ...)
204 if instanceof(class, AbstractValue) then
205 local obj = class(self.map, ...)
209 error("class must be a descendent of AbstractValue")
213 -- Parse optional options
214 function AbstractSection.parse_optionals(self, section)
215 if not self.optional then
219 self.optionals[section] = {}
221 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
222 for k,v in ipairs(self.children) do
223 if v.optional and not v:cfgvalue(section) then
224 if field == v.option then
227 table.insert(self.optionals[section], v)
232 if field and field:len() > 0 and self.dynamic then
233 self:add_dynamic(field)
237 -- Add a dynamic option
238 function AbstractSection.add_dynamic(self, field, optional)
239 local o = self:option(Value, field, field)
240 o.optional = optional
243 -- Parse all dynamic options
244 function AbstractSection.parse_dynamic(self, section)
245 if not self.dynamic then
249 local arr = ffluci.util.clone(self:cfgvalue(section))
250 local form = ffluci.http.formvalue("cbid."..self.config.."."..section)
251 if type(form) == "table" then
252 for k,v in pairs(form) do
257 for key,val in pairs(arr) do
260 for i,c in ipairs(self.children) do
261 if c.option == key then
266 if create and key:sub(1, 1) ~= "." then
267 self:add_dynamic(key, true)
272 -- Returns the section's UCI table
273 function AbstractSection.cfgvalue(self, section)
274 return self.map:get(section)
277 -- Removes the section
278 function AbstractSection.remove(self, section)
279 return self.map:del(section)
282 -- Creates the section
283 function AbstractSection.create(self, section)
284 return self.map:set(section, nil, self.sectiontype)
290 NamedSection - A fixed configuration section defined by its name
292 NamedSection = class(AbstractSection)
294 function NamedSection.__init__(self, map, section, ...)
295 AbstractSection.__init__(self, map, ...)
296 self.template = "cbi/nsection"
298 self.section = section
299 self.addremove = false
302 function NamedSection.parse(self)
303 local s = self.section
304 local active = self:cfgvalue(s)
307 if self.addremove then
308 local path = self.config.."."..s
309 if active then -- Remove the section
310 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
313 else -- Create and apply default values
314 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
315 for k,v in pairs(self.children) do
316 v:write(s, v.default)
323 AbstractSection.parse_dynamic(self, s)
324 if ffluci.http.formvalue("cbi.submit") then
327 AbstractSection.parse_optionals(self, s)
333 TypedSection - A (set of) configuration section(s) defined by the type
334 addremove: Defines whether the user can add/remove sections of this type
335 anonymous: Allow creating anonymous sections
336 validate: a validation function returning nil if the section is invalid
338 TypedSection = class(AbstractSection)
340 function TypedSection.__init__(self, ...)
341 AbstractSection.__init__(self, ...)
342 self.template = "cbi/tsection"
346 self.anonymous = false
349 -- Return all matching UCI sections for this TypedSection
350 function TypedSection.cfgsections(self)
352 for k, v in pairs(self.map:get()) do
353 if v[".type"] == self.sectiontype then
354 if self:checkscope(k) then
362 -- Creates a new section of this type with the given name (or anonymous)
363 function TypedSection.create(self, name)
365 self.map:set(name, nil, self.sectiontype)
367 name = self.map:add(self.sectiontype)
370 for k,v in pairs(self.children) do
372 self.map:set(name, v.option, v.default)
377 -- Limits scope to sections that have certain option => value pairs
378 function TypedSection.depends(self, option, value)
379 table.insert(self.deps, {option=option, value=value})
382 -- Excludes several sections by name
383 function TypedSection.exclude(self, field)
384 self.excludes[field] = true
387 function TypedSection.parse(self)
388 if self.addremove then
390 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
391 local name = ffluci.http.formvalue(crval)
392 if self.anonymous then
398 -- Ignore if it already exists
399 if self:cfgvalue(name) then
403 name = self:checkscope(name)
406 self.err_invalid = true
409 if name and name:len() > 0 then
416 crval = "cbi.rts." .. self.config
417 name = ffluci.http.formvalue(crval)
418 if type(name) == "table" then
419 for k,v in pairs(name) do
420 if self:cfgvalue(k) and self:checkscope(k) then
427 for k, v in pairs(self:cfgsections()) do
428 AbstractSection.parse_dynamic(self, k)
429 if ffluci.http.formvalue("cbi.submit") then
432 AbstractSection.parse_optionals(self, k)
436 -- Render the children
437 function TypedSection.render_children(self, section)
438 for k, node in ipairs(self.children) do
443 -- Verifies scope of sections
444 function TypedSection.checkscope(self, section)
445 -- Check if we are not excluded
446 if self.excludes[section] then
450 -- Check if at least one dependency is met
451 if #self.deps > 0 and self:cfgvalue(section) then
454 for k, v in ipairs(self.deps) do
455 if self:cfgvalue(section)[v.option] == v.value then
465 return self:validate(section)
469 -- Dummy validate function
470 function TypedSection.validate(self, section)
476 AbstractValue - An abstract Value Type
477 null: Value can be empty
478 valid: A function returning the value if it is valid otherwise nil
479 depends: A table of option => value pairs of which one must be true
480 default: The default value
481 size: The size of the input fields
482 rmempty: Unset value if empty
483 optional: This value is optional (see AbstractSection.optionals)
485 AbstractValue = class(Node)
487 function AbstractValue.__init__(self, map, option, ...)
488 Node.__init__(self, ...)
491 self.config = map.config
492 self.tag_invalid = {}
498 self.optional = false
501 -- Add a dependencie to another section field
502 function AbstractValue.depends(self, field, value)
503 table.insert(self.deps, {field=field, value=value})
506 -- Return whether this object should be created
507 function AbstractValue.formcreated(self, section)
508 local key = "cbi.opt."..self.config.."."..section
509 return (ffluci.http.formvalue(key) == self.option)
512 -- Returns the formvalue for this object
513 function AbstractValue.formvalue(self, section)
514 local key = "cbid."..self.map.config.."."..section.."."..self.option
515 return ffluci.http.formvalue(key)
518 function AbstractValue.parse(self, section)
519 local fvalue = self:formvalue(section)
521 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
522 fvalue = self:validate(fvalue)
524 self.tag_invalid[section] = true
526 if fvalue and not (fvalue == self:cfgvalue(section)) then
527 self:write(section, fvalue)
529 else -- Unset the UCI or error
530 if self.rmempty or self.optional then
536 -- Render if this value exists or if it is mandatory
537 function AbstractValue.render(self, s)
538 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
539 ffluci.template.render(self.template, {self=self, section=s})
543 -- Return the UCI value of this object
544 function AbstractValue.cfgvalue(self, section)
545 return self.map:get(section, self.option)
548 -- Validate the form value
549 function AbstractValue.validate(self, value)
554 function AbstractValue.write(self, section, value)
555 return self.map:set(section, self.option, value)
559 function AbstractValue.remove(self, section)
560 return self.map:del(section, self.option)
567 Value - A one-line value
568 maxlength: The maximum length
569 isnumber: The value must be a valid (floating point) number
570 isinteger: The value must be a valid integer
571 ispositive: The value must be positive (and a number)
573 Value = class(AbstractValue)
575 function Value.__init__(self, ...)
576 AbstractValue.__init__(self, ...)
577 self.template = "cbi/value"
580 self.isnumber = false
581 self.isinteger = false
584 -- This validation is a bit more complex
585 function Value.validate(self, val)
586 if self.maxlength and tostring(val):len() > self.maxlength then
590 return ffluci.util.validate(val, self.isnumber, self.isinteger)
594 -- DummyValue - This does nothing except being there
595 DummyValue = class(AbstractValue)
597 function DummyValue.__init__(self, map, ...)
598 AbstractValue.__init__(self, map, ...)
599 self.template = "cbi/dvalue"
603 function DummyValue.parse(self)
607 function DummyValue.render(self, s)
608 ffluci.template.render(self.template, {self=self, section=s})
613 Flag - A flag being enabled or disabled
615 Flag = class(AbstractValue)
617 function Flag.__init__(self, ...)
618 AbstractValue.__init__(self, ...)
619 self.template = "cbi/fvalue"
625 -- A flag can only have two states: set or unset
626 function Flag.parse(self, section)
627 local fvalue = self:formvalue(section)
630 fvalue = self.enabled
632 fvalue = self.disabled
635 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
636 if not(fvalue == self:cfgvalue(section)) then
637 self:write(section, fvalue)
647 ListValue - A one-line value predefined in a list
648 widget: The widget that will be used (select, radio)
650 ListValue = class(AbstractValue)
652 function ListValue.__init__(self, ...)
653 AbstractValue.__init__(self, ...)
654 self.template = "cbi/lvalue"
659 self.widget = "select"
662 function ListValue.value(self, key, val)
664 table.insert(self.keylist, tostring(key))
665 table.insert(self.vallist, tostring(val))
668 function ListValue.validate(self, val)
669 if ffluci.util.contains(self.keylist, val) then
679 MultiValue - Multiple delimited values
680 widget: The widget that will be used (select, checkbox)
681 delimiter: The delimiter that will separate the values (default: " ")
683 MultiValue = class(AbstractValue)
685 function MultiValue.__init__(self, ...)
686 AbstractValue.__init__(self, ...)
687 self.template = "cbi/mvalue"
691 self.widget = "checkbox"
695 function MultiValue.value(self, key, val)
697 table.insert(self.keylist, tostring(key))
698 table.insert(self.vallist, tostring(val))
701 function MultiValue.valuelist(self, section)
702 local val = self:cfgvalue(section)
704 if not(type(val) == "string") then
708 return ffluci.util.split(val, self.delimiter)
711 function MultiValue.validate(self, val)
712 if not(type(val) == "string") then
718 for value in val:gmatch("[^\n]+") do
719 if ffluci.util.contains(self.keylist, value) then
720 result = result .. self.delimiter .. value
724 if result:len() > 0 then
725 return result:sub(self.delimiter:len() + 1)