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")
50 ffluci.util.resfenv(func)
51 ffluci.util.updfenv(func, ffluci.cbi)
52 ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
56 if not instanceof(map, Map) then
57 error("CBI map returns no valid map object!")
61 ffluci.i18n.loadc("cbi")
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 self.ucidata = self.ucidata[self.config]
119 -- Creates a child section
120 function Map.section(self, class, ...)
121 if instanceof(class, AbstractSection) then
122 local obj = class(self, ...)
126 error("class must be a descendent of AbstractSection")
131 function Map.add(self, sectiontype)
132 local name = self.uci:add(self.config, sectiontype)
134 self.ucidata[name] = self.uci:show(self.config, name)
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
221 self.map:set(section, field, v.default)
224 table.insert(self.optionals[section], v)
229 if field and field:len() > 0 and self.dynamic then
230 self:add_dynamic(field)
234 -- Add a dynamic option
235 function AbstractSection.add_dynamic(self, field, optional)
236 local o = self:option(Value, field, field)
237 o.optional = optional
240 -- Parse all dynamic options
241 function AbstractSection.parse_dynamic(self, section)
242 if not self.dynamic then
246 local arr = ffluci.util.clone(self:cfgvalue(section))
247 local form = ffluci.http.formvalue("cbid."..self.config.."."..section)
248 if type(form) == "table" then
249 for k,v in pairs(form) do
254 for key,val in pairs(arr) do
257 for i,c in ipairs(self.children) do
258 if c.option == key then
263 if create and key:sub(1, 1) ~= "." then
264 self:add_dynamic(key, true)
269 -- Returns the section's UCI table
270 function AbstractSection.cfgvalue(self, section)
271 return self.map:get(section)
274 -- Removes the section
275 function AbstractSection.remove(self, section)
276 return self.map:del(section)
279 -- Creates the section
280 function AbstractSection.create(self, section)
281 return self.map:set(section, nil, self.sectiontype)
287 NamedSection - A fixed configuration section defined by its name
289 NamedSection = class(AbstractSection)
291 function NamedSection.__init__(self, map, section, ...)
292 AbstractSection.__init__(self, map, ...)
293 self.template = "cbi/nsection"
295 self.section = section
296 self.addremove = false
299 function NamedSection.parse(self)
300 local s = self.section
301 local active = self:cfgvalue(s)
304 if self.addremove then
305 local path = self.config.."."..s
306 if active then -- Remove the section
307 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
310 else -- Create and apply default values
311 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
312 for k,v in pairs(self.children) do
313 v:write(s, v.default)
320 AbstractSection.parse_dynamic(self, s)
322 AbstractSection.parse_optionals(self, s)
328 TypedSection - A (set of) configuration section(s) defined by the type
329 addremove: Defines whether the user can add/remove sections of this type
330 anonymous: Allow creating anonymous sections
331 valid: a list of names or a validation function for creating sections
332 scope: a list of names or a validation function for editing sections
334 TypedSection = class(AbstractSection)
336 function TypedSection.__init__(self, ...)
337 AbstractSection.__init__(self, ...)
338 self.template = "cbi/tsection"
340 self.anonymous = false
345 -- Creates a new section of this type with the given name (or anonymous)
346 function TypedSection.create(self, name)
348 self.map:set(name, nil, self.sectiontype)
350 name = self.map:add(self.sectiontype)
353 for k,v in pairs(self.children) do
355 self.map:set(name, v.option, v.default)
360 function TypedSection.parse(self)
361 if self.addremove then
363 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
364 local name = ffluci.http.formvalue(crval)
365 if self.anonymous then
371 name = ffluci.util.validate(name, self.valid)
373 self.err_invalid = true
375 if name and name:len() > 0 then
382 crval = "cbi.rts." .. self.config
383 name = ffluci.http.formvalue(crval)
384 if type(name) == "table" then
385 for k,v in pairs(name) do
386 if ffluci.util.validate(k, self.valid) then
393 for k, v in pairs(self:cfgsections()) do
394 AbstractSection.parse_dynamic(self, k)
396 AbstractSection.parse_optionals(self, k)
400 -- Render the children
401 function TypedSection.render_children(self, section)
402 for k, node in ipairs(self.children) do
407 -- Return all matching UCI sections for this TypedSection
408 function TypedSection.cfgsections(self)
410 for k, v in pairs(self.map:get()) do
411 if v[".type"] == self.sectiontype then
412 if ffluci.util.validate(k, self.scope) then
423 AbstractValue - An abstract Value Type
424 null: Value can be empty
425 valid: A function returning the value if it is valid otherwise nil
426 depends: A table of option => value pairs of which one must be true
427 default: The default value
428 size: The size of the input fields
429 rmempty: Unset value if empty
430 optional: This value is optional (see AbstractSection.optionals)
432 AbstractValue = class(Node)
434 function AbstractValue.__init__(self, map, option, ...)
435 Node.__init__(self, ...)
438 self.config = map.config
439 self.tag_invalid = {}
445 self.optional = false
448 -- Returns the formvalue for this object
449 function AbstractValue.formvalue(self, section)
450 local key = "cbid."..self.map.config.."."..section.."."..self.option
451 return ffluci.http.formvalue(key)
454 function AbstractValue.parse(self, section)
455 local fvalue = self:formvalue(section)
461 if fvalue then -- If we have a form value, validate it and write it to UCI
462 fvalue = self:validate(fvalue)
464 self.tag_invalid[section] = true
466 if fvalue and not (fvalue == self:cfgvalue(section)) then
467 self:write(section, fvalue)
469 elseif ffluci.http.formvalue("cbi.submit") then -- Unset the UCI or error
470 if self.rmempty or self.optional then
473 self.tag_invalid[section] = true
478 -- Render if this value exists or if it is mandatory
479 function AbstractValue.render(self, section)
480 if not self.optional or self:cfgvalue(section) then
481 ffluci.template.render(self.template, {self=self, section=section})
485 -- Return the UCI value of this object
486 function AbstractValue.cfgvalue(self, section)
487 return self.map:get(section, self.option)
490 -- Validate the form value
491 function AbstractValue.validate(self, val)
492 return ffluci.util.validate(val, self.valid)
496 function AbstractValue.write(self, section, value)
497 return self.map:set(section, self.option, value)
501 function AbstractValue.remove(self, section)
502 return self.map:del(section, self.option)
509 Value - A one-line value
510 maxlength: The maximum length
511 isnumber: The value must be a valid (floating point) number
512 isinteger: The value must be a valid integer
513 ispositive: The value must be positive (and a number)
515 Value = class(AbstractValue)
517 function Value.__init__(self, ...)
518 AbstractValue.__init__(self, ...)
519 self.template = "cbi/value"
522 self.isnumber = false
523 self.isinteger = false
526 -- This validation is a bit more complex
527 function Value.validate(self, val)
528 if self.maxlength and tostring(val):len() > self.maxlength then
532 return ffluci.util.validate(val, self.valid, self.isnumber, self.isinteger)
538 Flag - A flag being enabled or disabled
540 Flag = class(AbstractValue)
542 function Flag.__init__(self, ...)
543 AbstractValue.__init__(self, ...)
544 self.template = "cbi/fvalue"
550 -- A flag can only have two states: set or unset
551 function Flag.parse(self, section)
552 self.default = self.enabled
553 local fvalue = self:formvalue(section)
556 fvalue = self.enabled
558 fvalue = self.disabled
561 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
562 if not(fvalue == self:cfgvalue(section)) then
563 self:write(section, fvalue)
573 ListValue - A one-line value predefined in a list
574 widget: The widget that will be used (select, radio)
576 ListValue = class(AbstractValue)
578 function ListValue.__init__(self, ...)
579 AbstractValue.__init__(self, ...)
580 self.template = "cbi/lvalue"
585 self.widget = "select"
588 function ListValue.add_value(self, key, val)
590 table.insert(self.keylist, tostring(key))
591 table.insert(self.vallist, tostring(val))
594 function ListValue.validate(self, val)
595 if ffluci.util.contains(self.keylist, val) then
605 MultiValue - Multiple delimited values
606 widget: The widget that will be used (select, checkbox)
607 delimiter: The delimiter that will separate the values (default: " ")
609 MultiValue = class(AbstractValue)
611 function MultiValue.__init__(self, ...)
612 AbstractValue.__init__(self, ...)
613 self.template = "cbi/mvalue"
617 self.widget = "checkbox"
621 function MultiValue.add_value(self, key, val)
623 table.insert(self.keylist, tostring(key))
624 table.insert(self.vallist, tostring(val))
627 function MultiValue.valuelist(self, section)
628 local val = self:cfgvalue(section)
630 if not(type(val) == "string") then
634 return ffluci.util.split(val, self.delimiter)
637 function MultiValue.validate(self, val)
638 if not(type(val) == "string") then
644 for value in val:gmatch("[^\n]+") do
645 if ffluci.util.contains(self.keylist, value) then
646 result = result .. self.delimiter .. value
650 if result:len() > 0 then
651 return result:sub(self.delimiter:len() + 1)