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")
44 local cbidir = ffluci.sys.libpath() .. "/model/cbi/"
45 local func, err = loadfile(cbidir..cbimap..".lua")
51 ffluci.i18n.loadc("cbi")
53 ffluci.util.resfenv(func)
54 ffluci.util.updfenv(func, ffluci.cbi)
55 ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
59 if not instanceof(map, Map) then
60 error("CBI map returns no valid map object!")
67 -- Node pseudo abstract class
70 function Node.__init__(self, title, description)
72 self.title = title or ""
73 self.description = description or ""
74 self.template = "cbi/node"
78 function Node.append(self, obj)
79 table.insert(self.children, obj)
82 -- Parse this node and its children
83 function Node.parse(self, ...)
84 for k, child in ipairs(self.children) do
90 function Node.render(self)
91 ffluci.template.render(self.template, {self=self})
94 -- Render the children
95 function Node.render_children(self, ...)
96 for k, node in ipairs(self.children) do
103 A simple template element
105 Template = class(Node)
107 function Template.__init__(self, template)
109 self.template = template
114 Map - A map describing a configuration file
118 function Map.__init__(self, config, ...)
119 Node.__init__(self, ...)
121 self.template = "cbi/map"
122 self.uci = ffluci.model.uci.Session()
123 self.ucidata, self.uciorder = self.uci:sections(self.config)
124 if not self.ucidata or not self.uciorder then
125 error("Unable to read UCI data: " .. self.config)
129 -- Use optimized UCI writing
130 function Map.parse(self, ...)
131 self.uci:t_load(self.config)
132 Node.parse(self, ...)
133 self.uci:t_save(self.config)
136 -- Creates a child section
137 function Map.section(self, class, ...)
138 if instanceof(class, AbstractSection) then
139 local obj = class(self, ...)
143 error("class must be a descendent of AbstractSection")
148 function Map.add(self, sectiontype)
149 local name = self.uci:t_add(self.config, sectiontype)
151 self.ucidata[name] = {}
152 self.ucidata[name][".type"] = sectiontype
153 table.insert(self.uciorder, name)
159 function Map.set(self, section, option, value)
160 local stat = self.uci:t_set(self.config, section, option, value)
162 local val = self.uci:t_get(self.config, section, option)
164 self.ucidata[section][option] = val
166 if not self.ucidata[section] then
167 self.ucidata[section] = {}
169 self.ucidata[section][".type"] = val
170 table.insert(self.uciorder, section)
177 function Map.del(self, section, option)
178 local stat = self.uci:t_del(self.config, section, option)
181 self.ucidata[section][option] = nil
183 self.ucidata[section] = nil
184 for i, k in ipairs(self.uciorder) do
186 table.remove(self.uciorder, i)
195 function Map.get(self, section, option)
197 return self.ucidata, self.uciorder
198 elseif option and self.ucidata[section] then
199 return self.ucidata[section][option]
201 return self.ucidata[section]
209 AbstractSection = class(Node)
211 function AbstractSection.__init__(self, map, sectiontype, ...)
212 Node.__init__(self, ...)
213 self.sectiontype = sectiontype
215 self.config = map.config
219 self.addremove = false
223 -- Appends a new option
224 function AbstractSection.option(self, class, ...)
225 if instanceof(class, AbstractValue) then
226 local obj = class(self.map, ...)
230 error("class must be a descendent of AbstractValue")
234 -- Parse optional options
235 function AbstractSection.parse_optionals(self, section)
236 if not self.optional then
240 self.optionals[section] = {}
242 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
243 for k,v in ipairs(self.children) do
244 if v.optional and not v:cfgvalue(section) then
245 if field == v.option then
248 table.insert(self.optionals[section], v)
253 if field and #field > 0 and self.dynamic then
254 self:add_dynamic(field)
258 -- Add a dynamic option
259 function AbstractSection.add_dynamic(self, field, optional)
260 local o = self:option(Value, field, field)
261 o.optional = optional
264 -- Parse all dynamic options
265 function AbstractSection.parse_dynamic(self, section)
266 if not self.dynamic then
270 local arr = ffluci.util.clone(self:cfgvalue(section))
271 local form = ffluci.http.formvaluetable("cbid."..self.config.."."..section)
272 for k, v in pairs(form) do
276 for key,val in pairs(arr) do
279 for i,c in ipairs(self.children) do
280 if c.option == key then
285 if create and key:sub(1, 1) ~= "." then
286 self:add_dynamic(key, true)
291 -- Returns the section's UCI table
292 function AbstractSection.cfgvalue(self, section)
293 return self.map:get(section)
296 -- Removes the section
297 function AbstractSection.remove(self, section)
298 return self.map:del(section)
301 -- Creates the section
302 function AbstractSection.create(self, section)
303 return self.map:set(section, nil, self.sectiontype)
309 NamedSection - A fixed configuration section defined by its name
311 NamedSection = class(AbstractSection)
313 function NamedSection.__init__(self, map, section, ...)
314 AbstractSection.__init__(self, map, ...)
315 self.template = "cbi/nsection"
317 self.section = section
318 self.addremove = false
321 function NamedSection.parse(self)
322 local s = self.section
323 local active = self:cfgvalue(s)
326 if self.addremove then
327 local path = self.config.."."..s
328 if active then -- Remove the section
329 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
332 else -- Create and apply default values
333 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
334 for k,v in pairs(self.children) do
335 v:write(s, v.default)
342 AbstractSection.parse_dynamic(self, s)
343 if ffluci.http.formvalue("cbi.submit") then
346 AbstractSection.parse_optionals(self, s)
352 TypedSection - A (set of) configuration section(s) defined by the type
353 addremove: Defines whether the user can add/remove sections of this type
354 anonymous: Allow creating anonymous sections
355 validate: a validation function returning nil if the section is invalid
357 TypedSection = class(AbstractSection)
359 function TypedSection.__init__(self, ...)
360 AbstractSection.__init__(self, ...)
361 self.template = "cbi/tsection"
365 self.anonymous = false
368 -- Return all matching UCI sections for this TypedSection
369 function TypedSection.cfgsections(self)
371 local map, order = self.map:get()
373 for i, k in ipairs(order) do
374 if map[k][".type"] == self.sectiontype then
375 if self:checkscope(k) then
376 table.insert(sections, k)
384 -- Creates a new section of this type with the given name (or anonymous)
385 function TypedSection.create(self, name)
387 self.map:set(name, nil, self.sectiontype)
389 name = self.map:add(self.sectiontype)
392 for k,v in pairs(self.children) do
394 self.map:set(name, v.option, v.default)
399 -- Limits scope to sections that have certain option => value pairs
400 function TypedSection.depends(self, option, value)
401 table.insert(self.deps, {option=option, value=value})
404 -- Excludes several sections by name
405 function TypedSection.exclude(self, field)
406 self.excludes[field] = true
409 function TypedSection.parse(self)
410 if self.addremove then
412 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
413 local name = ffluci.http.formvalue(crval)
414 if self.anonymous then
420 -- Ignore if it already exists
421 if self:cfgvalue(name) then
425 name = self:checkscope(name)
428 self.err_invalid = true
431 if name and name:len() > 0 then
438 crval = "cbi.rts." .. self.config
439 name = ffluci.http.formvaluetable(crval)
440 for k,v in pairs(name) do
441 if self:cfgvalue(k) and self:checkscope(k) then
447 for i, k in ipairs(self:cfgsections()) do
448 AbstractSection.parse_dynamic(self, k)
449 if ffluci.http.formvalue("cbi.submit") then
452 AbstractSection.parse_optionals(self, k)
456 -- Render the children
457 function TypedSection.render_children(self, section)
458 for k, node in ipairs(self.children) do
463 -- Verifies scope of sections
464 function TypedSection.checkscope(self, section)
465 -- Check if we are not excluded
466 if self.excludes[section] then
470 -- Check if at least one dependency is met
471 if #self.deps > 0 and self:cfgvalue(section) then
474 for k, v in ipairs(self.deps) do
475 if self:cfgvalue(section)[v.option] == v.value then
485 return self:validate(section)
489 -- Dummy validate function
490 function TypedSection.validate(self, section)
496 AbstractValue - An abstract Value Type
497 null: Value can be empty
498 valid: A function returning the value if it is valid otherwise nil
499 depends: A table of option => value pairs of which one must be true
500 default: The default value
501 size: The size of the input fields
502 rmempty: Unset value if empty
503 optional: This value is optional (see AbstractSection.optionals)
505 AbstractValue = class(Node)
507 function AbstractValue.__init__(self, map, option, ...)
508 Node.__init__(self, ...)
511 self.config = map.config
512 self.tag_invalid = {}
518 self.optional = false
521 -- Add a dependencie to another section field
522 function AbstractValue.depends(self, field, value)
523 table.insert(self.deps, {field=field, value=value})
526 -- Return whether this object should be created
527 function AbstractValue.formcreated(self, section)
528 local key = "cbi.opt."..self.config.."."..section
529 return (ffluci.http.formvalue(key) == self.option)
532 -- Returns the formvalue for this object
533 function AbstractValue.formvalue(self, section)
534 local key = "cbid."..self.map.config.."."..section.."."..self.option
535 return ffluci.http.formvalue(key)
538 function AbstractValue.parse(self, section)
539 local fvalue = self:formvalue(section)
541 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
542 fvalue = self:validate(fvalue)
544 self.tag_invalid[section] = true
546 if fvalue and not (fvalue == self:cfgvalue(section)) then
547 self:write(section, fvalue)
549 else -- Unset the UCI or error
550 if self.rmempty or self.optional then
556 -- Render if this value exists or if it is mandatory
557 function AbstractValue.render(self, s)
558 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
559 ffluci.template.render(self.template, {self=self, section=s})
563 -- Return the UCI value of this object
564 function AbstractValue.cfgvalue(self, section)
565 return self.map:get(section, self.option)
568 -- Validate the form value
569 function AbstractValue.validate(self, value)
574 function AbstractValue.write(self, section, value)
575 return self.map:set(section, self.option, value)
579 function AbstractValue.remove(self, section)
580 return self.map:del(section, self.option)
587 Value - A one-line value
588 maxlength: The maximum length
589 isnumber: The value must be a valid (floating point) number
590 isinteger: The value must be a valid integer
591 ispositive: The value must be positive (and a number)
593 Value = class(AbstractValue)
595 function Value.__init__(self, ...)
596 AbstractValue.__init__(self, ...)
597 self.template = "cbi/value"
600 self.isnumber = false
601 self.isinteger = false
604 -- This validation is a bit more complex
605 function Value.validate(self, val)
606 if self.maxlength and tostring(val):len() > self.maxlength then
610 return ffluci.util.validate(val, self.isnumber, self.isinteger)
614 -- DummyValue - This does nothing except being there
615 DummyValue = class(AbstractValue)
617 function DummyValue.__init__(self, map, ...)
618 AbstractValue.__init__(self, map, ...)
619 self.template = "cbi/dvalue"
623 function DummyValue.parse(self)
627 function DummyValue.render(self, s)
628 ffluci.template.render(self.template, {self=self, section=s})
633 Flag - A flag being enabled or disabled
635 Flag = class(AbstractValue)
637 function Flag.__init__(self, ...)
638 AbstractValue.__init__(self, ...)
639 self.template = "cbi/fvalue"
645 -- A flag can only have two states: set or unset
646 function Flag.parse(self, section)
647 local fvalue = self:formvalue(section)
650 fvalue = self.enabled
652 fvalue = self.disabled
655 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
656 if not(fvalue == self:cfgvalue(section)) then
657 self:write(section, fvalue)
667 ListValue - A one-line value predefined in a list
668 widget: The widget that will be used (select, radio)
670 ListValue = class(AbstractValue)
672 function ListValue.__init__(self, ...)
673 AbstractValue.__init__(self, ...)
674 self.template = "cbi/lvalue"
679 self.widget = "select"
682 function ListValue.value(self, key, val)
684 table.insert(self.keylist, tostring(key))
685 table.insert(self.vallist, tostring(val))
688 function ListValue.validate(self, val)
689 if ffluci.util.contains(self.keylist, val) then
699 MultiValue - Multiple delimited values
700 widget: The widget that will be used (select, checkbox)
701 delimiter: The delimiter that will separate the values (default: " ")
703 MultiValue = class(AbstractValue)
705 function MultiValue.__init__(self, ...)
706 AbstractValue.__init__(self, ...)
707 self.template = "cbi/mvalue"
711 self.widget = "checkbox"
715 function MultiValue.value(self, key, val)
717 table.insert(self.keylist, tostring(key))
718 table.insert(self.vallist, tostring(val))
721 function MultiValue.valuelist(self, section)
722 local val = self:cfgvalue(section)
724 if not(type(val) == "string") then
728 return ffluci.util.split(val, self.delimiter)
731 function MultiValue.validate(self, val)
732 if not(type(val) == "string") then
738 for value in val:gmatch("[^\n]+") do
739 if ffluci.util.contains(self.keylist, value) then
740 result = result .. self.delimiter .. value
744 if result:len() > 0 then
745 return result:sub(self.delimiter:len() + 1)