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 A simple template element
104 Template = class(Node)
106 function Template.__init__(self, template)
108 self.template = template
113 Map - A map describing a configuration file
117 function Map.__init__(self, config, ...)
118 Node.__init__(self, ...)
120 self.template = "cbi/map"
121 self.uci = ffluci.model.uci.Session()
122 self.ucidata = self.uci:show(self.config)
123 if not self.ucidata then
124 error("Unable to read UCI data: " .. self.config)
126 if not self.ucidata[self.config] then
127 self.ucidata[self.config] = {}
129 self.ucidata = self.ucidata[self.config]
133 -- Creates a child section
134 function Map.section(self, class, ...)
135 if instanceof(class, AbstractSection) then
136 local obj = class(self, ...)
140 error("class must be a descendent of AbstractSection")
145 function Map.add(self, sectiontype)
146 local name = self.uci:add(self.config, sectiontype)
148 self.ucidata[name] = {}
149 self.ucidata[name][".type"] = sectiontype
155 function Map.set(self, section, option, value)
156 local stat = self.uci:set(self.config, section, option, value)
158 local val = self.uci:get(self.config, section, option)
160 self.ucidata[section][option] = val
162 if not self.ucidata[section] then
163 self.ucidata[section] = {}
165 self.ucidata[section][".type"] = val
172 function Map.del(self, section, option)
173 local stat = self.uci:del(self.config, section, option)
176 self.ucidata[section][option] = nil
178 self.ucidata[section] = nil
185 function Map.get(self, section, option)
188 elseif option and self.ucidata[section] then
189 return self.ucidata[section][option]
191 return self.ucidata[section]
199 AbstractSection = class(Node)
201 function AbstractSection.__init__(self, map, sectiontype, ...)
202 Node.__init__(self, ...)
203 self.sectiontype = sectiontype
205 self.config = map.config
209 self.addremove = false
213 -- Appends a new option
214 function AbstractSection.option(self, class, ...)
215 if instanceof(class, AbstractValue) then
216 local obj = class(self.map, ...)
220 error("class must be a descendent of AbstractValue")
224 -- Parse optional options
225 function AbstractSection.parse_optionals(self, section)
226 if not self.optional then
230 self.optionals[section] = {}
232 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
233 for k,v in ipairs(self.children) do
234 if v.optional and not v:cfgvalue(section) then
235 if field == v.option then
238 table.insert(self.optionals[section], v)
243 if field and field:len() > 0 and self.dynamic then
244 self:add_dynamic(field)
248 -- Add a dynamic option
249 function AbstractSection.add_dynamic(self, field, optional)
250 local o = self:option(Value, field, field)
251 o.optional = optional
254 -- Parse all dynamic options
255 function AbstractSection.parse_dynamic(self, section)
256 if not self.dynamic then
260 local arr = ffluci.util.clone(self:cfgvalue(section))
261 local form = ffluci.http.formvalue("cbid."..self.config.."."..section)
262 if type(form) == "table" then
263 for k,v in pairs(form) do
268 for key,val in pairs(arr) do
271 for i,c in ipairs(self.children) do
272 if c.option == key then
277 if create and key:sub(1, 1) ~= "." then
278 self:add_dynamic(key, true)
283 -- Returns the section's UCI table
284 function AbstractSection.cfgvalue(self, section)
285 return self.map:get(section)
288 -- Removes the section
289 function AbstractSection.remove(self, section)
290 return self.map:del(section)
293 -- Creates the section
294 function AbstractSection.create(self, section)
295 return self.map:set(section, nil, self.sectiontype)
301 NamedSection - A fixed configuration section defined by its name
303 NamedSection = class(AbstractSection)
305 function NamedSection.__init__(self, map, section, ...)
306 AbstractSection.__init__(self, map, ...)
307 self.template = "cbi/nsection"
309 self.section = section
310 self.addremove = false
313 function NamedSection.parse(self)
314 local s = self.section
315 local active = self:cfgvalue(s)
318 if self.addremove then
319 local path = self.config.."."..s
320 if active then -- Remove the section
321 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
324 else -- Create and apply default values
325 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
326 for k,v in pairs(self.children) do
327 v:write(s, v.default)
334 AbstractSection.parse_dynamic(self, s)
335 if ffluci.http.formvalue("cbi.submit") then
338 AbstractSection.parse_optionals(self, s)
344 TypedSection - A (set of) configuration section(s) defined by the type
345 addremove: Defines whether the user can add/remove sections of this type
346 anonymous: Allow creating anonymous sections
347 validate: a validation function returning nil if the section is invalid
349 TypedSection = class(AbstractSection)
351 function TypedSection.__init__(self, ...)
352 AbstractSection.__init__(self, ...)
353 self.template = "cbi/tsection"
357 self.anonymous = false
360 -- Return all matching UCI sections for this TypedSection
361 function TypedSection.cfgsections(self)
363 for k, v in pairs(self.map:get()) do
364 if v[".type"] == self.sectiontype then
365 if self:checkscope(k) then
373 -- Creates a new section of this type with the given name (or anonymous)
374 function TypedSection.create(self, name)
376 self.map:set(name, nil, self.sectiontype)
378 name = self.map:add(self.sectiontype)
381 for k,v in pairs(self.children) do
383 self.map:set(name, v.option, v.default)
388 -- Limits scope to sections that have certain option => value pairs
389 function TypedSection.depends(self, option, value)
390 table.insert(self.deps, {option=option, value=value})
393 -- Excludes several sections by name
394 function TypedSection.exclude(self, field)
395 self.excludes[field] = true
398 function TypedSection.parse(self)
399 if self.addremove then
401 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
402 local name = ffluci.http.formvalue(crval)
403 if self.anonymous then
409 -- Ignore if it already exists
410 if self:cfgvalue(name) then
414 name = self:checkscope(name)
417 self.err_invalid = true
420 if name and name:len() > 0 then
427 crval = "cbi.rts." .. self.config
428 name = ffluci.http.formvalue(crval)
429 if type(name) == "table" then
430 for k,v in pairs(name) do
431 if self:cfgvalue(k) and self:checkscope(k) then
438 for k, v in pairs(self:cfgsections()) do
439 AbstractSection.parse_dynamic(self, k)
440 if ffluci.http.formvalue("cbi.submit") then
443 AbstractSection.parse_optionals(self, k)
447 -- Render the children
448 function TypedSection.render_children(self, section)
449 for k, node in ipairs(self.children) do
454 -- Verifies scope of sections
455 function TypedSection.checkscope(self, section)
456 -- Check if we are not excluded
457 if self.excludes[section] then
461 -- Check if at least one dependency is met
462 if #self.deps > 0 and self:cfgvalue(section) then
465 for k, v in ipairs(self.deps) do
466 if self:cfgvalue(section)[v.option] == v.value then
476 return self:validate(section)
480 -- Dummy validate function
481 function TypedSection.validate(self, section)
487 AbstractValue - An abstract Value Type
488 null: Value can be empty
489 valid: A function returning the value if it is valid otherwise nil
490 depends: A table of option => value pairs of which one must be true
491 default: The default value
492 size: The size of the input fields
493 rmempty: Unset value if empty
494 optional: This value is optional (see AbstractSection.optionals)
496 AbstractValue = class(Node)
498 function AbstractValue.__init__(self, map, option, ...)
499 Node.__init__(self, ...)
502 self.config = map.config
503 self.tag_invalid = {}
509 self.optional = false
512 -- Add a dependencie to another section field
513 function AbstractValue.depends(self, field, value)
514 table.insert(self.deps, {field=field, value=value})
517 -- Return whether this object should be created
518 function AbstractValue.formcreated(self, section)
519 local key = "cbi.opt."..self.config.."."..section
520 return (ffluci.http.formvalue(key) == self.option)
523 -- Returns the formvalue for this object
524 function AbstractValue.formvalue(self, section)
525 local key = "cbid."..self.map.config.."."..section.."."..self.option
526 return ffluci.http.formvalue(key)
529 function AbstractValue.parse(self, section)
530 local fvalue = self:formvalue(section)
532 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
533 fvalue = self:validate(fvalue)
535 self.tag_invalid[section] = true
537 if fvalue and not (fvalue == self:cfgvalue(section)) then
538 self:write(section, fvalue)
540 else -- Unset the UCI or error
541 if self.rmempty or self.optional then
547 -- Render if this value exists or if it is mandatory
548 function AbstractValue.render(self, s)
549 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
550 ffluci.template.render(self.template, {self=self, section=s})
554 -- Return the UCI value of this object
555 function AbstractValue.cfgvalue(self, section)
556 return self.map:get(section, self.option)
559 -- Validate the form value
560 function AbstractValue.validate(self, value)
565 function AbstractValue.write(self, section, value)
566 return self.map:set(section, self.option, value)
570 function AbstractValue.remove(self, section)
571 return self.map:del(section, self.option)
578 Value - A one-line value
579 maxlength: The maximum length
580 isnumber: The value must be a valid (floating point) number
581 isinteger: The value must be a valid integer
582 ispositive: The value must be positive (and a number)
584 Value = class(AbstractValue)
586 function Value.__init__(self, ...)
587 AbstractValue.__init__(self, ...)
588 self.template = "cbi/value"
591 self.isnumber = false
592 self.isinteger = false
595 -- This validation is a bit more complex
596 function Value.validate(self, val)
597 if self.maxlength and tostring(val):len() > self.maxlength then
601 return ffluci.util.validate(val, self.isnumber, self.isinteger)
605 -- DummyValue - This does nothing except being there
606 DummyValue = class(AbstractValue)
608 function DummyValue.__init__(self, map, ...)
609 AbstractValue.__init__(self, map, ...)
610 self.template = "cbi/dvalue"
614 function DummyValue.parse(self)
618 function DummyValue.render(self, s)
619 ffluci.template.render(self.template, {self=self, section=s})
624 Flag - A flag being enabled or disabled
626 Flag = class(AbstractValue)
628 function Flag.__init__(self, ...)
629 AbstractValue.__init__(self, ...)
630 self.template = "cbi/fvalue"
636 -- A flag can only have two states: set or unset
637 function Flag.parse(self, section)
638 local fvalue = self:formvalue(section)
641 fvalue = self.enabled
643 fvalue = self.disabled
646 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
647 if not(fvalue == self:cfgvalue(section)) then
648 self:write(section, fvalue)
658 ListValue - A one-line value predefined in a list
659 widget: The widget that will be used (select, radio)
661 ListValue = class(AbstractValue)
663 function ListValue.__init__(self, ...)
664 AbstractValue.__init__(self, ...)
665 self.template = "cbi/lvalue"
670 self.widget = "select"
673 function ListValue.value(self, key, val)
675 table.insert(self.keylist, tostring(key))
676 table.insert(self.vallist, tostring(val))
679 function ListValue.validate(self, val)
680 if ffluci.util.contains(self.keylist, val) then
690 MultiValue - Multiple delimited values
691 widget: The widget that will be used (select, checkbox)
692 delimiter: The delimiter that will separate the values (default: " ")
694 MultiValue = class(AbstractValue)
696 function MultiValue.__init__(self, ...)
697 AbstractValue.__init__(self, ...)
698 self.template = "cbi/mvalue"
702 self.widget = "checkbox"
706 function MultiValue.value(self, key, val)
708 table.insert(self.keylist, tostring(key))
709 table.insert(self.vallist, tostring(val))
712 function MultiValue.valuelist(self, section)
713 local val = self:cfgvalue(section)
715 if not(type(val) == "string") then
719 return ffluci.util.split(val, self.delimiter)
722 function MultiValue.validate(self, val)
723 if not(type(val) == "string") then
729 for value in val:gmatch("[^\n]+") do
730 if ffluci.util.contains(self.keylist, value) then
731 result = result .. self.delimiter .. value
735 if result:len() > 0 then
736 return result:sub(self.delimiter:len() + 1)