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")
50 ffluci.util.resfenv(func)
51 ffluci.util.updfenv(func, ffluci.cbi)
52 ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
56 if not instanceof(map, Map) then
57 error("CBI map returns no valid map object!")
61 ffluci.i18n.loadc("cbi")
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 Map - A map describing a configuration file
106 function Map.__init__(self, config, ...)
107 Node.__init__(self, ...)
109 self.template = "cbi/map"
110 self.uci = ffluci.model.uci.Session()
111 self.ucidata = self.uci:show(self.config)
112 if not self.ucidata then
113 error("Unable to read UCI data: " .. self.config)
115 self.ucidata = self.ucidata[self.config]
119 -- Creates a child section
120 function Map.section(self, class, ...)
121 if instanceof(class, AbstractSection) then
122 local obj = class(self, ...)
126 error("class must be a descendent of AbstractSection")
131 function Map.add(self, sectiontype)
132 local name = self.uci:add(self.config, sectiontype)
134 self.ucidata[name] = self.uci:show(self.config, name)
140 function Map.set(self, section, option, value)
141 local stat = self.uci:set(self.config, section, option, value)
143 local val = self.uci:get(self.config, section, option)
145 self.ucidata[section][option] = val
147 if not self.ucidata[section] then
148 self.ucidata[section] = {}
150 self.ucidata[section][".type"] = val
157 function Map.del(self, section, option)
158 local stat = self.uci:del(self.config, section, option)
161 self.ucidata[section][option] = nil
163 self.ucidata[section] = nil
170 function Map.get(self, section, option)
173 elseif option and self.ucidata[section] then
174 return self.ucidata[section][option]
176 return self.ucidata[section]
184 AbstractSection = class(Node)
186 function AbstractSection.__init__(self, map, sectiontype, ...)
187 Node.__init__(self, ...)
188 self.sectiontype = sectiontype
190 self.config = map.config
194 self.addremove = false
198 -- Appends a new option
199 function AbstractSection.option(self, class, ...)
200 if instanceof(class, AbstractValue) then
201 local obj = class(self.map, ...)
205 error("class must be a descendent of AbstractValue")
209 -- Parse optional options
210 function AbstractSection.parse_optionals(self, section)
211 if not self.optional then
215 self.optionals[section] = {}
217 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
218 for k,v in ipairs(self.children) do
219 if v.optional and not v:cfgvalue(section) then
220 if field == v.option then
223 table.insert(self.optionals[section], v)
228 if field and field:len() > 0 and self.dynamic then
229 self:add_dynamic(field)
233 -- Add a dynamic option
234 function AbstractSection.add_dynamic(self, field, optional)
235 local o = self:option(Value, field, field)
236 o.optional = optional
239 -- Parse all dynamic options
240 function AbstractSection.parse_dynamic(self, section)
241 if not self.dynamic then
245 local arr = ffluci.util.clone(self:cfgvalue(section))
246 local form = ffluci.http.formvalue("cbid."..self.config.."."..section)
247 if type(form) == "table" then
248 for k,v in pairs(form) do
253 for key,val in pairs(arr) do
256 for i,c in ipairs(self.children) do
257 if c.option == key then
262 if create and key:sub(1, 1) ~= "." then
263 self:add_dynamic(key, true)
268 -- Returns the section's UCI table
269 function AbstractSection.cfgvalue(self, section)
270 return self.map:get(section)
273 -- Removes the section
274 function AbstractSection.remove(self, section)
275 return self.map:del(section)
278 -- Creates the section
279 function AbstractSection.create(self, section)
280 return self.map:set(section, nil, self.sectiontype)
286 NamedSection - A fixed configuration section defined by its name
288 NamedSection = class(AbstractSection)
290 function NamedSection.__init__(self, map, section, ...)
291 AbstractSection.__init__(self, map, ...)
292 self.template = "cbi/nsection"
294 self.section = section
295 self.addremove = false
298 function NamedSection.parse(self)
299 local s = self.section
300 local active = self:cfgvalue(s)
303 if self.addremove then
304 local path = self.config.."."..s
305 if active then -- Remove the section
306 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
309 else -- Create and apply default values
310 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
311 for k,v in pairs(self.children) do
312 v:write(s, v.default)
319 AbstractSection.parse_dynamic(self, s)
321 AbstractSection.parse_optionals(self, s)
327 TypedSection - A (set of) configuration section(s) defined by the type
328 addremove: Defines whether the user can add/remove sections of this type
329 anonymous: Allow creating anonymous sections
330 validate: a validation function returning nil if the section is invalid
332 TypedSection = class(AbstractSection)
334 function TypedSection.__init__(self, ...)
335 AbstractSection.__init__(self, ...)
336 self.template = "cbi/tsection"
340 self.anonymous = false
343 -- Return all matching UCI sections for this TypedSection
344 function TypedSection.cfgsections(self)
346 for k, v in pairs(self.map:get()) do
347 if v[".type"] == self.sectiontype then
348 if self:checkscope(k) then
356 -- Creates a new section of this type with the given name (or anonymous)
357 function TypedSection.create(self, name)
359 self.map:set(name, nil, self.sectiontype)
361 name = self.map:add(self.sectiontype)
364 for k,v in pairs(self.children) do
366 self.map:set(name, v.option, v.default)
371 -- Limits scope to sections that have certain option => value pairs
372 function TypedSection.depends(self, option, value)
373 table.insert(self.deps, {option=option, value=value})
376 -- Excludes several sections by name
377 function TypedSection.exclude(self, field)
378 self.excludes[field] = true
381 function TypedSection.parse(self)
382 if self.addremove then
384 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
385 local name = ffluci.http.formvalue(crval)
386 if self.anonymous then
392 -- Ignore if it already exists
393 if self:cfgvalue(name) then
397 name = self:checkscope(name)
400 self.err_invalid = true
403 if name and name:len() > 0 then
410 crval = "cbi.rts." .. self.config
411 name = ffluci.http.formvalue(crval)
412 if type(name) == "table" then
413 for k,v in pairs(name) do
414 if self:cfgvalue(k) and self:checkscope(k) then
421 for k, v in pairs(self:cfgsections()) do
422 AbstractSection.parse_dynamic(self, k)
424 AbstractSection.parse_optionals(self, k)
428 -- Render the children
429 function TypedSection.render_children(self, section)
430 for k, node in ipairs(self.children) do
435 -- Verifies scope of sections
436 function TypedSection.checkscope(self, section)
437 -- Check if we are not excluded
438 if self.excludes[section] then
442 -- Check if at least one dependency is met
443 if #self.deps > 0 and self:cfgvalue(section) then
446 for k, v in ipairs(self.deps) do
447 if self:cfgvalue(section)[v.option] == v.value then
457 return self:validate(section)
461 -- Dummy validate function
462 function TypedSection.validate(self, section)
468 AbstractValue - An abstract Value Type
469 null: Value can be empty
470 valid: A function returning the value if it is valid otherwise nil
471 depends: A table of option => value pairs of which one must be true
472 default: The default value
473 size: The size of the input fields
474 rmempty: Unset value if empty
475 optional: This value is optional (see AbstractSection.optionals)
477 AbstractValue = class(Node)
479 function AbstractValue.__init__(self, map, option, ...)
480 Node.__init__(self, ...)
483 self.config = map.config
484 self.tag_invalid = {}
490 self.optional = false
493 -- Add a dependencie to another section field
494 function AbstractValue.depends(self, field, value)
495 table.insert(self.deps, {field=field, value=value})
498 -- Return whether this object should be created
499 function AbstractValue.formcreated(self, section)
500 local key = "cbi.opt."..self.config.."."..section
501 return (ffluci.http.formvalue(key) == self.option)
504 -- Returns the formvalue for this object
505 function AbstractValue.formvalue(self, section)
506 local key = "cbid."..self.map.config.."."..section.."."..self.option
507 return ffluci.http.formvalue(key)
510 function AbstractValue.parse(self, section)
511 local fvalue = self:formvalue(section)
513 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
514 fvalue = self:validate(fvalue)
516 self.tag_invalid[section] = true
518 if fvalue and not (fvalue == self:cfgvalue(section)) then
519 self:write(section, fvalue)
521 elseif ffluci.http.formvalue("cbi.submit") then -- Unset the UCI or error
522 if self.rmempty or self.optional then
528 -- Render if this value exists or if it is mandatory
529 function AbstractValue.render(self, s)
530 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
531 ffluci.template.render(self.template, {self=self, section=s})
535 -- Return the UCI value of this object
536 function AbstractValue.cfgvalue(self, section)
537 return self.map:get(section, self.option)
540 -- Validate the form value
541 function AbstractValue.validate(self, value)
546 function AbstractValue.write(self, section, value)
547 return self.map:set(section, self.option, value)
551 function AbstractValue.remove(self, section)
552 return self.map:del(section, self.option)
559 Value - A one-line value
560 maxlength: The maximum length
561 isnumber: The value must be a valid (floating point) number
562 isinteger: The value must be a valid integer
563 ispositive: The value must be positive (and a number)
565 Value = class(AbstractValue)
567 function Value.__init__(self, ...)
568 AbstractValue.__init__(self, ...)
569 self.template = "cbi/value"
572 self.isnumber = false
573 self.isinteger = false
576 -- This validation is a bit more complex
577 function Value.validate(self, val)
578 if self.maxlength and tostring(val):len() > self.maxlength then
582 return ffluci.util.validate(val, self.isnumber, self.isinteger)
588 Flag - A flag being enabled or disabled
590 Flag = class(AbstractValue)
592 function Flag.__init__(self, ...)
593 AbstractValue.__init__(self, ...)
594 self.template = "cbi/fvalue"
600 -- A flag can only have two states: set or unset
601 function Flag.parse(self, section)
602 self.default = self.enabled
603 local fvalue = self:formvalue(section)
606 fvalue = self.enabled
608 fvalue = self.disabled
611 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
612 if not(fvalue == self:cfgvalue(section)) then
613 self:write(section, fvalue)
623 ListValue - A one-line value predefined in a list
624 widget: The widget that will be used (select, radio)
626 ListValue = class(AbstractValue)
628 function ListValue.__init__(self, ...)
629 AbstractValue.__init__(self, ...)
630 self.template = "cbi/lvalue"
635 self.widget = "select"
638 function ListValue.value(self, key, val)
640 table.insert(self.keylist, tostring(key))
641 table.insert(self.vallist, tostring(val))
644 function ListValue.validate(self, val)
645 if ffluci.util.contains(self.keylist, val) then
655 MultiValue - Multiple delimited values
656 widget: The widget that will be used (select, checkbox)
657 delimiter: The delimiter that will separate the values (default: " ")
659 MultiValue = class(AbstractValue)
661 function MultiValue.__init__(self, ...)
662 AbstractValue.__init__(self, ...)
663 self.template = "cbi/mvalue"
667 self.widget = "checkbox"
671 function MultiValue.value(self, key, val)
673 table.insert(self.keylist, tostring(key))
674 table.insert(self.vallist, tostring(val))
677 function MultiValue.valuelist(self, section)
678 local val = self:cfgvalue(section)
680 if not(type(val) == "string") then
684 return ffluci.util.split(val, self.delimiter)
687 function MultiValue.validate(self, val)
688 if not(type(val) == "string") then
694 for value in val:gmatch("[^\n]+") do
695 if ffluci.util.contains(self.keylist, value) then
696 result = result .. self.delimiter .. value
700 if result:len() > 0 then
701 return result:sub(self.delimiter:len() + 1)