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")
42 local cbidir = ffluci.fs.dirname(ffluci.util.__file__()) .. "/model/cbi/"
43 local func, err = loadfile(cbidir..cbimap..".lua")
49 ffluci.i18n.loadc("cbi")
51 ffluci.util.resfenv(func)
52 ffluci.util.updfenv(func, ffluci.cbi)
53 ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
57 if not instanceof(map, Map) then
58 error("CBI map returns no valid map object!")
65 -- Node pseudo abstract class
68 function Node.__init__(self, title, description)
70 self.title = title or ""
71 self.description = description or ""
72 self.template = "cbi/node"
76 function Node.append(self, obj)
77 table.insert(self.children, obj)
80 -- Parse this node and its children
81 function Node.parse(self, ...)
82 for k, child in ipairs(self.children) do
88 function Node.render(self)
89 ffluci.template.render(self.template, {self=self})
92 -- Render the children
93 function Node.render_children(self, ...)
94 for k, node in ipairs(self.children) do
101 Map - A map describing a configuration file
105 function Map.__init__(self, config, ...)
106 Node.__init__(self, ...)
108 self.template = "cbi/map"
109 self.uci = ffluci.model.uci.Session()
110 self.ucidata = self.uci:show(self.config)
111 if not self.ucidata then
112 error("Unable to read UCI data: " .. self.config)
114 self.ucidata = self.ucidata[self.config]
118 -- Creates a child section
119 function Map.section(self, class, ...)
120 if instanceof(class, AbstractSection) then
121 local obj = class(self, ...)
125 error("class must be a descendent of AbstractSection")
130 function Map.add(self, sectiontype)
131 local name = self.uci:add(self.config, sectiontype)
133 self.ucidata[name] = {}
134 self.ucidata[name][".type"] = sectiontype
140 function Map.set(self, section, option, value)
141 local stat = self.uci:set(self.config, section, option, value)
143 local val = self.uci:get(self.config, section, option)
145 self.ucidata[section][option] = val
147 if not self.ucidata[section] then
148 self.ucidata[section] = {}
150 self.ucidata[section][".type"] = val
157 function Map.del(self, section, option)
158 local stat = self.uci:del(self.config, section, option)
161 self.ucidata[section][option] = nil
163 self.ucidata[section] = nil
170 function Map.get(self, section, option)
173 elseif option and self.ucidata[section] then
174 return self.ucidata[section][option]
176 return self.ucidata[section]
184 AbstractSection = class(Node)
186 function AbstractSection.__init__(self, map, sectiontype, ...)
187 Node.__init__(self, ...)
188 self.sectiontype = sectiontype
190 self.config = map.config
194 self.addremove = false
198 -- Appends a new option
199 function AbstractSection.option(self, class, ...)
200 if instanceof(class, AbstractValue) then
201 local obj = class(self.map, ...)
205 error("class must be a descendent of AbstractValue")
209 -- Parse optional options
210 function AbstractSection.parse_optionals(self, section)
211 if not self.optional then
215 self.optionals[section] = {}
217 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
218 for k,v in ipairs(self.children) do
219 if v.optional and not v:cfgvalue(section) then
220 if field == v.option then
223 table.insert(self.optionals[section], v)
228 if field and field:len() > 0 and self.dynamic then
229 self:add_dynamic(field)
233 -- Add a dynamic option
234 function AbstractSection.add_dynamic(self, field, optional)
235 local o = self:option(Value, field, field)
236 o.optional = optional
239 -- Parse all dynamic options
240 function AbstractSection.parse_dynamic(self, section)
241 if not self.dynamic then
245 local arr = ffluci.util.clone(self:cfgvalue(section))
246 local form = ffluci.http.formvalue("cbid."..self.config.."."..section)
247 if type(form) == "table" then
248 for k,v in pairs(form) do
253 for key,val in pairs(arr) do
256 for i,c in ipairs(self.children) do
257 if c.option == key then
262 if create and key:sub(1, 1) ~= "." then
263 self:add_dynamic(key, true)
268 -- Returns the section's UCI table
269 function AbstractSection.cfgvalue(self, section)
270 return self.map:get(section)
273 -- Removes the section
274 function AbstractSection.remove(self, section)
275 return self.map:del(section)
278 -- Creates the section
279 function AbstractSection.create(self, section)
280 return self.map:set(section, nil, self.sectiontype)
286 NamedSection - A fixed configuration section defined by its name
288 NamedSection = class(AbstractSection)
290 function NamedSection.__init__(self, map, section, ...)
291 AbstractSection.__init__(self, map, ...)
292 self.template = "cbi/nsection"
294 self.section = section
295 self.addremove = false
298 function NamedSection.parse(self)
299 local s = self.section
300 local active = self:cfgvalue(s)
303 if self.addremove then
304 local path = self.config.."."..s
305 if active then -- Remove the section
306 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
309 else -- Create and apply default values
310 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
311 for k,v in pairs(self.children) do
312 v:write(s, v.default)
319 AbstractSection.parse_dynamic(self, s)
320 if ffluci.http.formvalue("cbi.submit") then
323 AbstractSection.parse_optionals(self, s)
329 TypedSection - A (set of) configuration section(s) defined by the type
330 addremove: Defines whether the user can add/remove sections of this type
331 anonymous: Allow creating anonymous sections
332 validate: a validation function returning nil if the section is invalid
334 TypedSection = class(AbstractSection)
336 function TypedSection.__init__(self, ...)
337 AbstractSection.__init__(self, ...)
338 self.template = "cbi/tsection"
342 self.anonymous = false
345 -- Return all matching UCI sections for this TypedSection
346 function TypedSection.cfgsections(self)
348 for k, v in pairs(self.map:get()) do
349 if v[".type"] == self.sectiontype then
350 if self:checkscope(k) then
358 -- Creates a new section of this type with the given name (or anonymous)
359 function TypedSection.create(self, name)
361 self.map:set(name, nil, self.sectiontype)
363 name = self.map:add(self.sectiontype)
366 for k,v in pairs(self.children) do
368 self.map:set(name, v.option, v.default)
373 -- Limits scope to sections that have certain option => value pairs
374 function TypedSection.depends(self, option, value)
375 table.insert(self.deps, {option=option, value=value})
378 -- Excludes several sections by name
379 function TypedSection.exclude(self, field)
380 self.excludes[field] = true
383 function TypedSection.parse(self)
384 if self.addremove then
386 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
387 local name = ffluci.http.formvalue(crval)
388 if self.anonymous then
394 -- Ignore if it already exists
395 if self:cfgvalue(name) then
399 name = self:checkscope(name)
402 self.err_invalid = true
405 if name and name:len() > 0 then
412 crval = "cbi.rts." .. self.config
413 name = ffluci.http.formvalue(crval)
414 if type(name) == "table" then
415 for k,v in pairs(name) do
416 if self:cfgvalue(k) and self:checkscope(k) then
423 for k, v in pairs(self:cfgsections()) do
424 AbstractSection.parse_dynamic(self, k)
425 if ffluci.http.formvalue("cbi.submit") then
428 AbstractSection.parse_optionals(self, k)
432 -- Render the children
433 function TypedSection.render_children(self, section)
434 for k, node in ipairs(self.children) do
439 -- Verifies scope of sections
440 function TypedSection.checkscope(self, section)
441 -- Check if we are not excluded
442 if self.excludes[section] then
446 -- Check if at least one dependency is met
447 if #self.deps > 0 and self:cfgvalue(section) then
450 for k, v in ipairs(self.deps) do
451 if self:cfgvalue(section)[v.option] == v.value then
461 return self:validate(section)
465 -- Dummy validate function
466 function TypedSection.validate(self, section)
472 AbstractValue - An abstract Value Type
473 null: Value can be empty
474 valid: A function returning the value if it is valid otherwise nil
475 depends: A table of option => value pairs of which one must be true
476 default: The default value
477 size: The size of the input fields
478 rmempty: Unset value if empty
479 optional: This value is optional (see AbstractSection.optionals)
481 AbstractValue = class(Node)
483 function AbstractValue.__init__(self, map, option, ...)
484 Node.__init__(self, ...)
487 self.config = map.config
488 self.tag_invalid = {}
494 self.optional = false
497 -- Add a dependencie to another section field
498 function AbstractValue.depends(self, field, value)
499 table.insert(self.deps, {field=field, value=value})
502 -- Return whether this object should be created
503 function AbstractValue.formcreated(self, section)
504 local key = "cbi.opt."..self.config.."."..section
505 return (ffluci.http.formvalue(key) == self.option)
508 -- Returns the formvalue for this object
509 function AbstractValue.formvalue(self, section)
510 local key = "cbid."..self.map.config.."."..section.."."..self.option
511 return ffluci.http.formvalue(key)
514 function AbstractValue.parse(self, section)
515 local fvalue = self:formvalue(section)
517 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
518 fvalue = self:validate(fvalue)
520 self.tag_invalid[section] = true
522 if fvalue and not (fvalue == self:cfgvalue(section)) then
523 self:write(section, fvalue)
525 else -- Unset the UCI or error
526 if self.rmempty or self.optional then
532 -- Render if this value exists or if it is mandatory
533 function AbstractValue.render(self, s)
534 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
535 ffluci.template.render(self.template, {self=self, section=s})
539 -- Return the UCI value of this object
540 function AbstractValue.cfgvalue(self, section)
541 return self.map:get(section, self.option)
544 -- Validate the form value
545 function AbstractValue.validate(self, value)
550 function AbstractValue.write(self, section, value)
551 return self.map:set(section, self.option, value)
555 function AbstractValue.remove(self, section)
556 return self.map:del(section, self.option)
563 Value - A one-line value
564 maxlength: The maximum length
565 isnumber: The value must be a valid (floating point) number
566 isinteger: The value must be a valid integer
567 ispositive: The value must be positive (and a number)
569 Value = class(AbstractValue)
571 function Value.__init__(self, ...)
572 AbstractValue.__init__(self, ...)
573 self.template = "cbi/value"
576 self.isnumber = false
577 self.isinteger = false
580 -- This validation is a bit more complex
581 function Value.validate(self, val)
582 if self.maxlength and tostring(val):len() > self.maxlength then
586 return ffluci.util.validate(val, self.isnumber, self.isinteger)
590 -- DummyValue - This does nothing except being there
591 DummyValue = class(AbstractValue)
593 function DummyValue.__init__(self, map, ...)
594 AbstractValue.__init__(self, map, ...)
595 self.template = "cbi/dvalue"
599 function DummyValue.parse(self)
603 function DummyValue.render(self, s)
604 ffluci.template.render(self.template, {self=self, section=s})
609 Flag - A flag being enabled or disabled
611 Flag = class(AbstractValue)
613 function Flag.__init__(self, ...)
614 AbstractValue.__init__(self, ...)
615 self.template = "cbi/fvalue"
621 -- A flag can only have two states: set or unset
622 function Flag.parse(self, section)
623 local fvalue = self:formvalue(section)
626 fvalue = self.enabled
628 fvalue = self.disabled
631 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
632 if not(fvalue == self:cfgvalue(section)) then
633 self:write(section, fvalue)
643 ListValue - A one-line value predefined in a list
644 widget: The widget that will be used (select, radio)
646 ListValue = class(AbstractValue)
648 function ListValue.__init__(self, ...)
649 AbstractValue.__init__(self, ...)
650 self.template = "cbi/lvalue"
655 self.widget = "select"
658 function ListValue.value(self, key, val)
660 table.insert(self.keylist, tostring(key))
661 table.insert(self.vallist, tostring(val))
664 function ListValue.validate(self, val)
665 if ffluci.util.contains(self.keylist, val) then
675 MultiValue - Multiple delimited values
676 widget: The widget that will be used (select, checkbox)
677 delimiter: The delimiter that will separate the values (default: " ")
679 MultiValue = class(AbstractValue)
681 function MultiValue.__init__(self, ...)
682 AbstractValue.__init__(self, ...)
683 self.template = "cbi/mvalue"
687 self.widget = "checkbox"
691 function MultiValue.value(self, key, val)
693 table.insert(self.keylist, tostring(key))
694 table.insert(self.vallist, tostring(val))
697 function MultiValue.valuelist(self, section)
698 local val = self:cfgvalue(section)
700 if not(type(val) == "string") then
704 return ffluci.util.split(val, self.delimiter)
707 function MultiValue.validate(self, val)
708 if not(type(val) == "string") then
714 for value in val:gmatch("[^\n]+") do
715 if ffluci.util.contains(self.keylist, value) then
716 result = result .. self.delimiter .. value
720 if result:len() > 0 then
721 return result:sub(self.delimiter:len() + 1)