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 if not self.ucidata[self.config] then
115 self.ucidata[self.config] = {}
117 self.ucidata = self.ucidata[self.config]
121 -- Creates a child section
122 function Map.section(self, class, ...)
123 if instanceof(class, AbstractSection) then
124 local obj = class(self, ...)
128 error("class must be a descendent of AbstractSection")
133 function Map.add(self, sectiontype)
134 local name = self.uci:add(self.config, sectiontype)
136 self.ucidata[name] = {}
137 self.ucidata[name][".type"] = sectiontype
143 function Map.set(self, section, option, value)
144 local stat = self.uci:set(self.config, section, option, value)
146 local val = self.uci:get(self.config, section, option)
148 self.ucidata[section][option] = val
150 if not self.ucidata[section] then
151 self.ucidata[section] = {}
153 self.ucidata[section][".type"] = val
160 function Map.del(self, section, option)
161 local stat = self.uci:del(self.config, section, option)
164 self.ucidata[section][option] = nil
166 self.ucidata[section] = nil
173 function Map.get(self, section, option)
176 elseif option and self.ucidata[section] then
177 return self.ucidata[section][option]
179 return self.ucidata[section]
187 AbstractSection = class(Node)
189 function AbstractSection.__init__(self, map, sectiontype, ...)
190 Node.__init__(self, ...)
191 self.sectiontype = sectiontype
193 self.config = map.config
197 self.addremove = false
201 -- Appends a new option
202 function AbstractSection.option(self, class, ...)
203 if instanceof(class, AbstractValue) then
204 local obj = class(self.map, ...)
208 error("class must be a descendent of AbstractValue")
212 -- Parse optional options
213 function AbstractSection.parse_optionals(self, section)
214 if not self.optional then
218 self.optionals[section] = {}
220 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
221 for k,v in ipairs(self.children) do
222 if v.optional and not v:cfgvalue(section) then
223 if field == v.option then
226 table.insert(self.optionals[section], v)
231 if field and field:len() > 0 and self.dynamic then
232 self:add_dynamic(field)
236 -- Add a dynamic option
237 function AbstractSection.add_dynamic(self, field, optional)
238 local o = self:option(Value, field, field)
239 o.optional = optional
242 -- Parse all dynamic options
243 function AbstractSection.parse_dynamic(self, section)
244 if not self.dynamic then
248 local arr = ffluci.util.clone(self:cfgvalue(section))
249 local form = ffluci.http.formvalue("cbid."..self.config.."."..section)
250 if type(form) == "table" then
251 for k,v in pairs(form) do
256 for key,val in pairs(arr) do
259 for i,c in ipairs(self.children) do
260 if c.option == key then
265 if create and key:sub(1, 1) ~= "." then
266 self:add_dynamic(key, true)
271 -- Returns the section's UCI table
272 function AbstractSection.cfgvalue(self, section)
273 return self.map:get(section)
276 -- Removes the section
277 function AbstractSection.remove(self, section)
278 return self.map:del(section)
281 -- Creates the section
282 function AbstractSection.create(self, section)
283 return self.map:set(section, nil, self.sectiontype)
289 NamedSection - A fixed configuration section defined by its name
291 NamedSection = class(AbstractSection)
293 function NamedSection.__init__(self, map, section, ...)
294 AbstractSection.__init__(self, map, ...)
295 self.template = "cbi/nsection"
297 self.section = section
298 self.addremove = false
301 function NamedSection.parse(self)
302 local s = self.section
303 local active = self:cfgvalue(s)
306 if self.addremove then
307 local path = self.config.."."..s
308 if active then -- Remove the section
309 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
312 else -- Create and apply default values
313 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
314 for k,v in pairs(self.children) do
315 v:write(s, v.default)
322 AbstractSection.parse_dynamic(self, s)
323 if ffluci.http.formvalue("cbi.submit") then
326 AbstractSection.parse_optionals(self, s)
332 TypedSection - A (set of) configuration section(s) defined by the type
333 addremove: Defines whether the user can add/remove sections of this type
334 anonymous: Allow creating anonymous sections
335 validate: a validation function returning nil if the section is invalid
337 TypedSection = class(AbstractSection)
339 function TypedSection.__init__(self, ...)
340 AbstractSection.__init__(self, ...)
341 self.template = "cbi/tsection"
345 self.anonymous = false
348 -- Return all matching UCI sections for this TypedSection
349 function TypedSection.cfgsections(self)
351 for k, v in pairs(self.map:get()) do
352 if v[".type"] == self.sectiontype then
353 if self:checkscope(k) then
361 -- Creates a new section of this type with the given name (or anonymous)
362 function TypedSection.create(self, name)
364 self.map:set(name, nil, self.sectiontype)
366 name = self.map:add(self.sectiontype)
369 for k,v in pairs(self.children) do
371 self.map:set(name, v.option, v.default)
376 -- Limits scope to sections that have certain option => value pairs
377 function TypedSection.depends(self, option, value)
378 table.insert(self.deps, {option=option, value=value})
381 -- Excludes several sections by name
382 function TypedSection.exclude(self, field)
383 self.excludes[field] = true
386 function TypedSection.parse(self)
387 if self.addremove then
389 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
390 local name = ffluci.http.formvalue(crval)
391 if self.anonymous then
397 -- Ignore if it already exists
398 if self:cfgvalue(name) then
402 name = self:checkscope(name)
405 self.err_invalid = true
408 if name and name:len() > 0 then
415 crval = "cbi.rts." .. self.config
416 name = ffluci.http.formvalue(crval)
417 if type(name) == "table" then
418 for k,v in pairs(name) do
419 if self:cfgvalue(k) and self:checkscope(k) then
426 for k, v in pairs(self:cfgsections()) do
427 AbstractSection.parse_dynamic(self, k)
428 if ffluci.http.formvalue("cbi.submit") then
431 AbstractSection.parse_optionals(self, k)
435 -- Render the children
436 function TypedSection.render_children(self, section)
437 for k, node in ipairs(self.children) do
442 -- Verifies scope of sections
443 function TypedSection.checkscope(self, section)
444 -- Check if we are not excluded
445 if self.excludes[section] then
449 -- Check if at least one dependency is met
450 if #self.deps > 0 and self:cfgvalue(section) then
453 for k, v in ipairs(self.deps) do
454 if self:cfgvalue(section)[v.option] == v.value then
464 return self:validate(section)
468 -- Dummy validate function
469 function TypedSection.validate(self, section)
475 AbstractValue - An abstract Value Type
476 null: Value can be empty
477 valid: A function returning the value if it is valid otherwise nil
478 depends: A table of option => value pairs of which one must be true
479 default: The default value
480 size: The size of the input fields
481 rmempty: Unset value if empty
482 optional: This value is optional (see AbstractSection.optionals)
484 AbstractValue = class(Node)
486 function AbstractValue.__init__(self, map, option, ...)
487 Node.__init__(self, ...)
490 self.config = map.config
491 self.tag_invalid = {}
497 self.optional = false
500 -- Add a dependencie to another section field
501 function AbstractValue.depends(self, field, value)
502 table.insert(self.deps, {field=field, value=value})
505 -- Return whether this object should be created
506 function AbstractValue.formcreated(self, section)
507 local key = "cbi.opt."..self.config.."."..section
508 return (ffluci.http.formvalue(key) == self.option)
511 -- Returns the formvalue for this object
512 function AbstractValue.formvalue(self, section)
513 local key = "cbid."..self.map.config.."."..section.."."..self.option
514 return ffluci.http.formvalue(key)
517 function AbstractValue.parse(self, section)
518 local fvalue = self:formvalue(section)
520 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
521 fvalue = self:validate(fvalue)
523 self.tag_invalid[section] = true
525 if fvalue and not (fvalue == self:cfgvalue(section)) then
526 self:write(section, fvalue)
528 else -- Unset the UCI or error
529 if self.rmempty or self.optional then
535 -- Render if this value exists or if it is mandatory
536 function AbstractValue.render(self, s)
537 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
538 ffluci.template.render(self.template, {self=self, section=s})
542 -- Return the UCI value of this object
543 function AbstractValue.cfgvalue(self, section)
544 return self.map:get(section, self.option)
547 -- Validate the form value
548 function AbstractValue.validate(self, value)
553 function AbstractValue.write(self, section, value)
554 return self.map:set(section, self.option, value)
558 function AbstractValue.remove(self, section)
559 return self.map:del(section, self.option)
566 Value - A one-line value
567 maxlength: The maximum length
568 isnumber: The value must be a valid (floating point) number
569 isinteger: The value must be a valid integer
570 ispositive: The value must be positive (and a number)
572 Value = class(AbstractValue)
574 function Value.__init__(self, ...)
575 AbstractValue.__init__(self, ...)
576 self.template = "cbi/value"
579 self.isnumber = false
580 self.isinteger = false
583 -- This validation is a bit more complex
584 function Value.validate(self, val)
585 if self.maxlength and tostring(val):len() > self.maxlength then
589 return ffluci.util.validate(val, self.isnumber, self.isinteger)
593 -- DummyValue - This does nothing except being there
594 DummyValue = class(AbstractValue)
596 function DummyValue.__init__(self, map, ...)
597 AbstractValue.__init__(self, map, ...)
598 self.template = "cbi/dvalue"
602 function DummyValue.parse(self)
606 function DummyValue.render(self, s)
607 ffluci.template.render(self.template, {self=self, section=s})
612 Flag - A flag being enabled or disabled
614 Flag = class(AbstractValue)
616 function Flag.__init__(self, ...)
617 AbstractValue.__init__(self, ...)
618 self.template = "cbi/fvalue"
624 -- A flag can only have two states: set or unset
625 function Flag.parse(self, section)
626 local fvalue = self:formvalue(section)
629 fvalue = self.enabled
631 fvalue = self.disabled
634 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
635 if not(fvalue == self:cfgvalue(section)) then
636 self:write(section, fvalue)
646 ListValue - A one-line value predefined in a list
647 widget: The widget that will be used (select, radio)
649 ListValue = class(AbstractValue)
651 function ListValue.__init__(self, ...)
652 AbstractValue.__init__(self, ...)
653 self.template = "cbi/lvalue"
658 self.widget = "select"
661 function ListValue.value(self, key, val)
663 table.insert(self.keylist, tostring(key))
664 table.insert(self.vallist, tostring(val))
667 function ListValue.validate(self, val)
668 if ffluci.util.contains(self.keylist, val) then
678 MultiValue - Multiple delimited values
679 widget: The widget that will be used (select, checkbox)
680 delimiter: The delimiter that will separate the values (default: " ")
682 MultiValue = class(AbstractValue)
684 function MultiValue.__init__(self, ...)
685 AbstractValue.__init__(self, ...)
686 self.template = "cbi/mvalue"
690 self.widget = "checkbox"
694 function MultiValue.value(self, key, val)
696 table.insert(self.keylist, tostring(key))
697 table.insert(self.vallist, tostring(val))
700 function MultiValue.valuelist(self, section)
701 local val = self:cfgvalue(section)
703 if not(type(val) == "string") then
707 return ffluci.util.split(val, self.delimiter)
710 function MultiValue.validate(self, val)
711 if not(type(val) == "string") then
717 for value in val:gmatch("[^\n]+") do
718 if ffluci.util.contains(self.keylist, value) then
719 result = result .. self.delimiter .. value
723 if result:len() > 0 then
724 return result:sub(self.delimiter:len() + 1)