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.uciorder = self.uci:sections(self.config)
123 if not self.ucidata or not self.uciorder then
124 error("Unable to read UCI data: " .. self.config)
128 -- Use optimized UCI writing
129 function Map.parse(self, ...)
130 self.uci:t_load(self.config)
131 Node.parse(self, ...)
132 self.uci:t_save(self.config)
135 -- Creates a child section
136 function Map.section(self, class, ...)
137 if instanceof(class, AbstractSection) then
138 local obj = class(self, ...)
142 error("class must be a descendent of AbstractSection")
147 function Map.add(self, sectiontype)
148 local name = self.uci:t_add(self.config, sectiontype)
150 self.ucidata[name] = {}
151 self.ucidata[name][".type"] = sectiontype
152 table.insert(self.uciorder, name)
158 function Map.set(self, section, option, value)
159 local stat = self.uci:t_set(self.config, section, option, value)
161 local val = self.uci:t_get(self.config, section, option)
163 self.ucidata[section][option] = val
165 if not self.ucidata[section] then
166 self.ucidata[section] = {}
168 self.ucidata[section][".type"] = val
169 table.insert(self.uciorder, section)
176 function Map.del(self, section, option)
177 local stat = self.uci:t_del(self.config, section, option)
180 self.ucidata[section][option] = nil
182 self.ucidata[section] = nil
183 for i, k in ipairs(self.uciorder) do
185 table.remove(self.uciorder, i)
194 function Map.get(self, section, option)
196 return self.ucidata, self.uciorder
197 elseif option and self.ucidata[section] then
198 return self.ucidata[section][option]
200 return self.ucidata[section]
208 AbstractSection = class(Node)
210 function AbstractSection.__init__(self, map, sectiontype, ...)
211 Node.__init__(self, ...)
212 self.sectiontype = sectiontype
214 self.config = map.config
218 self.addremove = false
222 -- Appends a new option
223 function AbstractSection.option(self, class, ...)
224 if instanceof(class, AbstractValue) then
225 local obj = class(self.map, ...)
229 error("class must be a descendent of AbstractValue")
233 -- Parse optional options
234 function AbstractSection.parse_optionals(self, section)
235 if not self.optional then
239 self.optionals[section] = {}
241 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
242 for k,v in ipairs(self.children) do
243 if v.optional and not v:cfgvalue(section) then
244 if field == v.option then
247 table.insert(self.optionals[section], v)
252 if field and #field > 0 and self.dynamic then
253 self:add_dynamic(field)
257 -- Add a dynamic option
258 function AbstractSection.add_dynamic(self, field, optional)
259 local o = self:option(Value, field, field)
260 o.optional = optional
263 -- Parse all dynamic options
264 function AbstractSection.parse_dynamic(self, section)
265 if not self.dynamic then
269 local arr = ffluci.util.clone(self:cfgvalue(section))
270 local form = ffluci.http.formvaluetable("cbid."..self.config.."."..section)
271 for k, v in pairs(form) do
275 for key,val in pairs(arr) do
278 for i,c in ipairs(self.children) do
279 if c.option == key then
284 if create and key:sub(1, 1) ~= "." then
285 self:add_dynamic(key, true)
290 -- Returns the section's UCI table
291 function AbstractSection.cfgvalue(self, section)
292 return self.map:get(section)
295 -- Removes the section
296 function AbstractSection.remove(self, section)
297 return self.map:del(section)
300 -- Creates the section
301 function AbstractSection.create(self, section)
302 return self.map:set(section, nil, self.sectiontype)
308 NamedSection - A fixed configuration section defined by its name
310 NamedSection = class(AbstractSection)
312 function NamedSection.__init__(self, map, section, ...)
313 AbstractSection.__init__(self, map, ...)
314 self.template = "cbi/nsection"
316 self.section = section
317 self.addremove = false
320 function NamedSection.parse(self)
321 local s = self.section
322 local active = self:cfgvalue(s)
325 if self.addremove then
326 local path = self.config.."."..s
327 if active then -- Remove the section
328 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
331 else -- Create and apply default values
332 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
333 for k,v in pairs(self.children) do
334 v:write(s, v.default)
341 AbstractSection.parse_dynamic(self, s)
342 if ffluci.http.formvalue("cbi.submit") then
345 AbstractSection.parse_optionals(self, s)
351 TypedSection - A (set of) configuration section(s) defined by the type
352 addremove: Defines whether the user can add/remove sections of this type
353 anonymous: Allow creating anonymous sections
354 validate: a validation function returning nil if the section is invalid
356 TypedSection = class(AbstractSection)
358 function TypedSection.__init__(self, ...)
359 AbstractSection.__init__(self, ...)
360 self.template = "cbi/tsection"
364 self.anonymous = false
367 -- Return all matching UCI sections for this TypedSection
368 function TypedSection.cfgsections(self)
370 local map, order = self.map:get()
372 for i, k in ipairs(order) do
373 if map[k][".type"] == self.sectiontype then
374 if self:checkscope(k) then
375 table.insert(sections, k)
383 -- Creates a new section of this type with the given name (or anonymous)
384 function TypedSection.create(self, name)
386 self.map:set(name, nil, self.sectiontype)
388 name = self.map:add(self.sectiontype)
391 for k,v in pairs(self.children) do
393 self.map:set(name, v.option, v.default)
398 -- Limits scope to sections that have certain option => value pairs
399 function TypedSection.depends(self, option, value)
400 table.insert(self.deps, {option=option, value=value})
403 -- Excludes several sections by name
404 function TypedSection.exclude(self, field)
405 self.excludes[field] = true
408 function TypedSection.parse(self)
409 if self.addremove then
411 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
412 local name = ffluci.http.formvalue(crval)
413 if self.anonymous then
419 -- Ignore if it already exists
420 if self:cfgvalue(name) then
424 name = self:checkscope(name)
427 self.err_invalid = true
430 if name and name:len() > 0 then
437 crval = "cbi.rts." .. self.config
438 name = ffluci.http.formvaluetable(crval)
439 for k,v in pairs(name) do
440 if self:cfgvalue(k) and self:checkscope(k) then
446 for i, k in ipairs(self:cfgsections()) do
447 AbstractSection.parse_dynamic(self, k)
448 if ffluci.http.formvalue("cbi.submit") then
451 AbstractSection.parse_optionals(self, k)
455 -- Render the children
456 function TypedSection.render_children(self, section)
457 for k, node in ipairs(self.children) do
462 -- Verifies scope of sections
463 function TypedSection.checkscope(self, section)
464 -- Check if we are not excluded
465 if self.excludes[section] then
469 -- Check if at least one dependency is met
470 if #self.deps > 0 and self:cfgvalue(section) then
473 for k, v in ipairs(self.deps) do
474 if self:cfgvalue(section)[v.option] == v.value then
484 return self:validate(section)
488 -- Dummy validate function
489 function TypedSection.validate(self, section)
495 AbstractValue - An abstract Value Type
496 null: Value can be empty
497 valid: A function returning the value if it is valid otherwise nil
498 depends: A table of option => value pairs of which one must be true
499 default: The default value
500 size: The size of the input fields
501 rmempty: Unset value if empty
502 optional: This value is optional (see AbstractSection.optionals)
504 AbstractValue = class(Node)
506 function AbstractValue.__init__(self, map, option, ...)
507 Node.__init__(self, ...)
510 self.config = map.config
511 self.tag_invalid = {}
517 self.optional = false
520 -- Add a dependencie to another section field
521 function AbstractValue.depends(self, field, value)
522 table.insert(self.deps, {field=field, value=value})
525 -- Return whether this object should be created
526 function AbstractValue.formcreated(self, section)
527 local key = "cbi.opt."..self.config.."."..section
528 return (ffluci.http.formvalue(key) == self.option)
531 -- Returns the formvalue for this object
532 function AbstractValue.formvalue(self, section)
533 local key = "cbid."..self.map.config.."."..section.."."..self.option
534 return ffluci.http.formvalue(key)
537 function AbstractValue.parse(self, section)
538 local fvalue = self:formvalue(section)
540 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
541 fvalue = self:validate(fvalue)
543 self.tag_invalid[section] = true
545 if fvalue and not (fvalue == self:cfgvalue(section)) then
546 self:write(section, fvalue)
548 else -- Unset the UCI or error
549 if self.rmempty or self.optional then
555 -- Render if this value exists or if it is mandatory
556 function AbstractValue.render(self, s)
557 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
558 ffluci.template.render(self.template, {self=self, section=s})
562 -- Return the UCI value of this object
563 function AbstractValue.cfgvalue(self, section)
564 return self.map:get(section, self.option)
567 -- Validate the form value
568 function AbstractValue.validate(self, value)
573 function AbstractValue.write(self, section, value)
574 return self.map:set(section, self.option, value)
578 function AbstractValue.remove(self, section)
579 return self.map:del(section, self.option)
586 Value - A one-line value
587 maxlength: The maximum length
588 isnumber: The value must be a valid (floating point) number
589 isinteger: The value must be a valid integer
590 ispositive: The value must be positive (and a number)
592 Value = class(AbstractValue)
594 function Value.__init__(self, ...)
595 AbstractValue.__init__(self, ...)
596 self.template = "cbi/value"
599 self.isnumber = false
600 self.isinteger = false
603 -- This validation is a bit more complex
604 function Value.validate(self, val)
605 if self.maxlength and tostring(val):len() > self.maxlength then
609 return ffluci.util.validate(val, self.isnumber, self.isinteger)
613 -- DummyValue - This does nothing except being there
614 DummyValue = class(AbstractValue)
616 function DummyValue.__init__(self, map, ...)
617 AbstractValue.__init__(self, map, ...)
618 self.template = "cbi/dvalue"
622 function DummyValue.parse(self)
626 function DummyValue.render(self, s)
627 ffluci.template.render(self.template, {self=self, section=s})
632 Flag - A flag being enabled or disabled
634 Flag = class(AbstractValue)
636 function Flag.__init__(self, ...)
637 AbstractValue.__init__(self, ...)
638 self.template = "cbi/fvalue"
644 -- A flag can only have two states: set or unset
645 function Flag.parse(self, section)
646 local fvalue = self:formvalue(section)
649 fvalue = self.enabled
651 fvalue = self.disabled
654 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
655 if not(fvalue == self:cfgvalue(section)) then
656 self:write(section, fvalue)
666 ListValue - A one-line value predefined in a list
667 widget: The widget that will be used (select, radio)
669 ListValue = class(AbstractValue)
671 function ListValue.__init__(self, ...)
672 AbstractValue.__init__(self, ...)
673 self.template = "cbi/lvalue"
678 self.widget = "select"
681 function ListValue.value(self, key, val)
683 table.insert(self.keylist, tostring(key))
684 table.insert(self.vallist, tostring(val))
687 function ListValue.validate(self, val)
688 if ffluci.util.contains(self.keylist, val) then
698 MultiValue - Multiple delimited values
699 widget: The widget that will be used (select, checkbox)
700 delimiter: The delimiter that will separate the values (default: " ")
702 MultiValue = class(AbstractValue)
704 function MultiValue.__init__(self, ...)
705 AbstractValue.__init__(self, ...)
706 self.template = "cbi/mvalue"
710 self.widget = "checkbox"
714 function MultiValue.value(self, key, val)
716 table.insert(self.keylist, tostring(key))
717 table.insert(self.vallist, tostring(val))
720 function MultiValue.valuelist(self, section)
721 local val = self:cfgvalue(section)
723 if not(type(val) == "string") then
727 return ffluci.util.split(val, self.delimiter)
730 function MultiValue.validate(self, val)
731 if not(type(val) == "string") then
737 for value in val:gmatch("[^\n]+") do
738 if ffluci.util.contains(self.keylist, value) then
739 result = result .. self.delimiter .. value
743 if result:len() > 0 then
744 return result:sub(self.delimiter:len() + 1)