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
150 self.ucidata[".order"] = self.ucidata[".order"] or {}
151 table.insert(self.ucidata[".order"], name)
157 function Map.set(self, section, option, value)
158 local stat = self.uci:set(self.config, section, option, value)
160 local val = self.uci:get(self.config, section, option)
162 self.ucidata[section][option] = val
164 if not self.ucidata[section] then
165 self.ucidata[section] = {}
167 self.ucidata[section][".type"] = val
168 self.ucidata[".order"] = self.ucidata[".order"] or {}
169 table.insert(self.ucidata[".order"], section)
176 function Map.del(self, section, option)
177 local stat = self.uci:del(self.config, section, option)
180 self.ucidata[section][option] = nil
182 self.ucidata[section] = nil
183 for i, k in ipairs(self.ucidata[".order"]) do
185 table.remove(self.ucidata[".order"], i)
194 function Map.get(self, section, option)
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:len() > 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.formvalue("cbid."..self.config.."."..section)
271 if type(form) == "table" then
272 for k,v in pairs(form) do
277 for key,val in pairs(arr) do
280 for i,c in ipairs(self.children) do
281 if c.option == key then
286 if create and key:sub(1, 1) ~= "." then
287 self:add_dynamic(key, true)
292 -- Returns the section's UCI table
293 function AbstractSection.cfgvalue(self, section)
294 return self.map:get(section)
297 -- Removes the section
298 function AbstractSection.remove(self, section)
299 return self.map:del(section)
302 -- Creates the section
303 function AbstractSection.create(self, section)
304 return self.map:set(section, nil, self.sectiontype)
310 NamedSection - A fixed configuration section defined by its name
312 NamedSection = class(AbstractSection)
314 function NamedSection.__init__(self, map, section, ...)
315 AbstractSection.__init__(self, map, ...)
316 self.template = "cbi/nsection"
318 self.section = section
319 self.addremove = false
322 function NamedSection.parse(self)
323 local s = self.section
324 local active = self:cfgvalue(s)
327 if self.addremove then
328 local path = self.config.."."..s
329 if active then -- Remove the section
330 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
333 else -- Create and apply default values
334 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
335 for k,v in pairs(self.children) do
336 v:write(s, v.default)
343 AbstractSection.parse_dynamic(self, s)
344 if ffluci.http.formvalue("cbi.submit") then
347 AbstractSection.parse_optionals(self, s)
353 TypedSection - A (set of) configuration section(s) defined by the type
354 addremove: Defines whether the user can add/remove sections of this type
355 anonymous: Allow creating anonymous sections
356 validate: a validation function returning nil if the section is invalid
358 TypedSection = class(AbstractSection)
360 function TypedSection.__init__(self, ...)
361 AbstractSection.__init__(self, ...)
362 self.template = "cbi/tsection"
366 self.anonymous = false
369 -- Return all matching UCI sections for this TypedSection
370 function TypedSection.cfgsections(self)
373 local map = self.map:get()
374 if not map[".order"] then
378 for i, k in pairs(map[".order"]) do
379 if map[k][".type"] == self.sectiontype then
380 if self:checkscope(k) then
381 table.insert(sections, k)
388 -- Creates a new section of this type with the given name (or anonymous)
389 function TypedSection.create(self, name)
391 self.map:set(name, nil, self.sectiontype)
393 name = self.map:add(self.sectiontype)
396 for k,v in pairs(self.children) do
398 self.map:set(name, v.option, v.default)
403 -- Limits scope to sections that have certain option => value pairs
404 function TypedSection.depends(self, option, value)
405 table.insert(self.deps, {option=option, value=value})
408 -- Excludes several sections by name
409 function TypedSection.exclude(self, field)
410 self.excludes[field] = true
413 function TypedSection.parse(self)
414 if self.addremove then
416 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
417 local name = ffluci.http.formvalue(crval)
418 if self.anonymous then
424 -- Ignore if it already exists
425 if self:cfgvalue(name) then
429 name = self:checkscope(name)
432 self.err_invalid = true
435 if name and name:len() > 0 then
442 crval = "cbi.rts." .. self.config
443 name = ffluci.http.formvalue(crval)
444 if type(name) == "table" then
445 for k,v in pairs(name) do
446 if self:cfgvalue(k) and self:checkscope(k) then
453 for i, k in ipairs(self:cfgsections()) do
454 AbstractSection.parse_dynamic(self, k)
455 if ffluci.http.formvalue("cbi.submit") then
458 AbstractSection.parse_optionals(self, k)
462 -- Render the children
463 function TypedSection.render_children(self, section)
464 for k, node in ipairs(self.children) do
469 -- Verifies scope of sections
470 function TypedSection.checkscope(self, section)
471 -- Check if we are not excluded
472 if self.excludes[section] then
476 -- Check if at least one dependency is met
477 if #self.deps > 0 and self:cfgvalue(section) then
480 for k, v in ipairs(self.deps) do
481 if self:cfgvalue(section)[v.option] == v.value then
491 return self:validate(section)
495 -- Dummy validate function
496 function TypedSection.validate(self, section)
502 AbstractValue - An abstract Value Type
503 null: Value can be empty
504 valid: A function returning the value if it is valid otherwise nil
505 depends: A table of option => value pairs of which one must be true
506 default: The default value
507 size: The size of the input fields
508 rmempty: Unset value if empty
509 optional: This value is optional (see AbstractSection.optionals)
511 AbstractValue = class(Node)
513 function AbstractValue.__init__(self, map, option, ...)
514 Node.__init__(self, ...)
517 self.config = map.config
518 self.tag_invalid = {}
524 self.optional = false
527 -- Add a dependencie to another section field
528 function AbstractValue.depends(self, field, value)
529 table.insert(self.deps, {field=field, value=value})
532 -- Return whether this object should be created
533 function AbstractValue.formcreated(self, section)
534 local key = "cbi.opt."..self.config.."."..section
535 return (ffluci.http.formvalue(key) == self.option)
538 -- Returns the formvalue for this object
539 function AbstractValue.formvalue(self, section)
540 local key = "cbid."..self.map.config.."."..section.."."..self.option
541 return ffluci.http.formvalue(key)
544 function AbstractValue.parse(self, section)
545 local fvalue = self:formvalue(section)
547 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
548 fvalue = self:validate(fvalue)
550 self.tag_invalid[section] = true
552 if fvalue and not (fvalue == self:cfgvalue(section)) then
553 self:write(section, fvalue)
555 else -- Unset the UCI or error
556 if self.rmempty or self.optional then
562 -- Render if this value exists or if it is mandatory
563 function AbstractValue.render(self, s)
564 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
565 ffluci.template.render(self.template, {self=self, section=s})
569 -- Return the UCI value of this object
570 function AbstractValue.cfgvalue(self, section)
571 return self.map:get(section, self.option)
574 -- Validate the form value
575 function AbstractValue.validate(self, value)
580 function AbstractValue.write(self, section, value)
581 return self.map:set(section, self.option, value)
585 function AbstractValue.remove(self, section)
586 return self.map:del(section, self.option)
593 Value - A one-line value
594 maxlength: The maximum length
595 isnumber: The value must be a valid (floating point) number
596 isinteger: The value must be a valid integer
597 ispositive: The value must be positive (and a number)
599 Value = class(AbstractValue)
601 function Value.__init__(self, ...)
602 AbstractValue.__init__(self, ...)
603 self.template = "cbi/value"
606 self.isnumber = false
607 self.isinteger = false
610 -- This validation is a bit more complex
611 function Value.validate(self, val)
612 if self.maxlength and tostring(val):len() > self.maxlength then
616 return ffluci.util.validate(val, self.isnumber, self.isinteger)
620 -- DummyValue - This does nothing except being there
621 DummyValue = class(AbstractValue)
623 function DummyValue.__init__(self, map, ...)
624 AbstractValue.__init__(self, map, ...)
625 self.template = "cbi/dvalue"
629 function DummyValue.parse(self)
633 function DummyValue.render(self, s)
634 ffluci.template.render(self.template, {self=self, section=s})
639 Flag - A flag being enabled or disabled
641 Flag = class(AbstractValue)
643 function Flag.__init__(self, ...)
644 AbstractValue.__init__(self, ...)
645 self.template = "cbi/fvalue"
651 -- A flag can only have two states: set or unset
652 function Flag.parse(self, section)
653 local fvalue = self:formvalue(section)
656 fvalue = self.enabled
658 fvalue = self.disabled
661 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
662 if not(fvalue == self:cfgvalue(section)) then
663 self:write(section, fvalue)
673 ListValue - A one-line value predefined in a list
674 widget: The widget that will be used (select, radio)
676 ListValue = class(AbstractValue)
678 function ListValue.__init__(self, ...)
679 AbstractValue.__init__(self, ...)
680 self.template = "cbi/lvalue"
685 self.widget = "select"
688 function ListValue.value(self, key, val)
690 table.insert(self.keylist, tostring(key))
691 table.insert(self.vallist, tostring(val))
694 function ListValue.validate(self, val)
695 if ffluci.util.contains(self.keylist, val) then
705 MultiValue - Multiple delimited values
706 widget: The widget that will be used (select, checkbox)
707 delimiter: The delimiter that will separate the values (default: " ")
709 MultiValue = class(AbstractValue)
711 function MultiValue.__init__(self, ...)
712 AbstractValue.__init__(self, ...)
713 self.template = "cbi/mvalue"
717 self.widget = "checkbox"
721 function MultiValue.value(self, key, val)
723 table.insert(self.keylist, tostring(key))
724 table.insert(self.vallist, tostring(val))
727 function MultiValue.valuelist(self, section)
728 local val = self:cfgvalue(section)
730 if not(type(val) == "string") then
734 return ffluci.util.split(val, self.delimiter)
737 function MultiValue.validate(self, val)
738 if not(type(val) == "string") then
744 for value in val:gmatch("[^\n]+") do
745 if ffluci.util.contains(self.keylist, value) then
746 result = result .. self.delimiter .. value
750 if result:len() > 0 then
751 return result:sub(self.delimiter:len() + 1)