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:sections(self.config)
123 if not self.ucidata then
124 error("Unable to read UCI data: " .. self.config)
128 -- Creates a child section
129 function Map.section(self, class, ...)
130 if instanceof(class, AbstractSection) then
131 local obj = class(self, ...)
135 error("class must be a descendent of AbstractSection")
140 function Map.add(self, sectiontype)
141 local name = self.uci:add(self.config, sectiontype)
143 self.ucidata[name] = {}
144 self.ucidata[name][".type"] = sectiontype
145 self.ucidata[".order"] = self.ucidata[".order"] or {}
146 table.insert(self.ucidata[".order"], name)
152 function Map.set(self, section, option, value)
153 local stat = self.uci:set(self.config, section, option, value)
155 local val = self.uci:get(self.config, section, option)
157 self.ucidata[section][option] = val
159 if not self.ucidata[section] then
160 self.ucidata[section] = {}
162 self.ucidata[section][".type"] = val
163 self.ucidata[".order"] = self.ucidata[".order"] or {}
164 table.insert(self.ucidata[".order"], section)
171 function Map.del(self, section, option)
172 local stat = self.uci:del(self.config, section, option)
175 self.ucidata[section][option] = nil
177 self.ucidata[section] = nil
178 for i, k in ipairs(self.ucidata[".order"]) do
180 table.remove(self.ucidata[".order"], i)
189 function Map.get(self, section, option)
192 elseif option and self.ucidata[section] then
193 return self.ucidata[section][option]
195 return self.ucidata[section]
203 AbstractSection = class(Node)
205 function AbstractSection.__init__(self, map, sectiontype, ...)
206 Node.__init__(self, ...)
207 self.sectiontype = sectiontype
209 self.config = map.config
213 self.addremove = false
217 -- Appends a new option
218 function AbstractSection.option(self, class, ...)
219 if instanceof(class, AbstractValue) then
220 local obj = class(self.map, ...)
224 error("class must be a descendent of AbstractValue")
228 -- Parse optional options
229 function AbstractSection.parse_optionals(self, section)
230 if not self.optional then
234 self.optionals[section] = {}
236 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
237 for k,v in ipairs(self.children) do
238 if v.optional and not v:cfgvalue(section) then
239 if field == v.option then
242 table.insert(self.optionals[section], v)
247 if field and #field > 0 and self.dynamic then
248 self:add_dynamic(field)
252 -- Add a dynamic option
253 function AbstractSection.add_dynamic(self, field, optional)
254 local o = self:option(Value, field, field)
255 o.optional = optional
258 -- Parse all dynamic options
259 function AbstractSection.parse_dynamic(self, section)
260 if not self.dynamic then
264 local arr = ffluci.util.clone(self:cfgvalue(section))
265 local form = ffluci.http.formvaluetable("cbid."..self.config.."."..section)
266 for k, v in pairs(form) do
270 for key,val in pairs(arr) do
273 for i,c in ipairs(self.children) do
274 if c.option == key then
279 if create and key:sub(1, 1) ~= "." then
280 self:add_dynamic(key, true)
285 -- Returns the section's UCI table
286 function AbstractSection.cfgvalue(self, section)
287 return self.map:get(section)
290 -- Removes the section
291 function AbstractSection.remove(self, section)
292 return self.map:del(section)
295 -- Creates the section
296 function AbstractSection.create(self, section)
297 return self.map:set(section, nil, self.sectiontype)
303 NamedSection - A fixed configuration section defined by its name
305 NamedSection = class(AbstractSection)
307 function NamedSection.__init__(self, map, section, ...)
308 AbstractSection.__init__(self, map, ...)
309 self.template = "cbi/nsection"
311 self.section = section
312 self.addremove = false
315 function NamedSection.parse(self)
316 local s = self.section
317 local active = self:cfgvalue(s)
320 if self.addremove then
321 local path = self.config.."."..s
322 if active then -- Remove the section
323 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
326 else -- Create and apply default values
327 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
328 for k,v in pairs(self.children) do
329 v:write(s, v.default)
336 AbstractSection.parse_dynamic(self, s)
337 if ffluci.http.formvalue("cbi.submit") then
340 AbstractSection.parse_optionals(self, s)
346 TypedSection - A (set of) configuration section(s) defined by the type
347 addremove: Defines whether the user can add/remove sections of this type
348 anonymous: Allow creating anonymous sections
349 validate: a validation function returning nil if the section is invalid
351 TypedSection = class(AbstractSection)
353 function TypedSection.__init__(self, ...)
354 AbstractSection.__init__(self, ...)
355 self.template = "cbi/tsection"
359 self.anonymous = false
362 -- Return all matching UCI sections for this TypedSection
363 function TypedSection.cfgsections(self)
366 local map = self.map:get()
367 if not map[".order"] then
371 for i, k in pairs(map[".order"]) do
372 if map[k][".type"] == self.sectiontype then
373 if self:checkscope(k) then
374 table.insert(sections, k)
381 -- Creates a new section of this type with the given name (or anonymous)
382 function TypedSection.create(self, name)
384 self.map:set(name, nil, self.sectiontype)
386 name = self.map:add(self.sectiontype)
389 for k,v in pairs(self.children) do
391 self.map:set(name, v.option, v.default)
396 -- Limits scope to sections that have certain option => value pairs
397 function TypedSection.depends(self, option, value)
398 table.insert(self.deps, {option=option, value=value})
401 -- Excludes several sections by name
402 function TypedSection.exclude(self, field)
403 self.excludes[field] = true
406 function TypedSection.parse(self)
407 if self.addremove then
409 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
410 local name = ffluci.http.formvalue(crval)
411 if self.anonymous then
417 -- Ignore if it already exists
418 if self:cfgvalue(name) then
422 name = self:checkscope(name)
425 self.err_invalid = true
428 if name and name:len() > 0 then
435 crval = "cbi.rts." .. self.config
436 name = ffluci.http.formvaluetable(crval)
437 for k,v in pairs(name) do
438 if self:cfgvalue(k) and self:checkscope(k) then
444 for i, k in ipairs(self:cfgsections()) do
445 AbstractSection.parse_dynamic(self, k)
446 if ffluci.http.formvalue("cbi.submit") then
449 AbstractSection.parse_optionals(self, k)
453 -- Render the children
454 function TypedSection.render_children(self, section)
455 for k, node in ipairs(self.children) do
460 -- Verifies scope of sections
461 function TypedSection.checkscope(self, section)
462 -- Check if we are not excluded
463 if self.excludes[section] then
467 -- Check if at least one dependency is met
468 if #self.deps > 0 and self:cfgvalue(section) then
471 for k, v in ipairs(self.deps) do
472 if self:cfgvalue(section)[v.option] == v.value then
482 return self:validate(section)
486 -- Dummy validate function
487 function TypedSection.validate(self, section)
493 AbstractValue - An abstract Value Type
494 null: Value can be empty
495 valid: A function returning the value if it is valid otherwise nil
496 depends: A table of option => value pairs of which one must be true
497 default: The default value
498 size: The size of the input fields
499 rmempty: Unset value if empty
500 optional: This value is optional (see AbstractSection.optionals)
502 AbstractValue = class(Node)
504 function AbstractValue.__init__(self, map, option, ...)
505 Node.__init__(self, ...)
508 self.config = map.config
509 self.tag_invalid = {}
515 self.optional = false
518 -- Add a dependencie to another section field
519 function AbstractValue.depends(self, field, value)
520 table.insert(self.deps, {field=field, value=value})
523 -- Return whether this object should be created
524 function AbstractValue.formcreated(self, section)
525 local key = "cbi.opt."..self.config.."."..section
526 return (ffluci.http.formvalue(key) == self.option)
529 -- Returns the formvalue for this object
530 function AbstractValue.formvalue(self, section)
531 local key = "cbid."..self.map.config.."."..section.."."..self.option
532 return ffluci.http.formvalue(key)
535 function AbstractValue.parse(self, section)
536 local fvalue = self:formvalue(section)
538 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
539 fvalue = self:validate(fvalue)
541 self.tag_invalid[section] = true
543 if fvalue and not (fvalue == self:cfgvalue(section)) then
544 self:write(section, fvalue)
546 else -- Unset the UCI or error
547 if self.rmempty or self.optional then
553 -- Render if this value exists or if it is mandatory
554 function AbstractValue.render(self, s)
555 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
556 ffluci.template.render(self.template, {self=self, section=s})
560 -- Return the UCI value of this object
561 function AbstractValue.cfgvalue(self, section)
562 return self.map:get(section, self.option)
565 -- Validate the form value
566 function AbstractValue.validate(self, value)
571 function AbstractValue.write(self, section, value)
572 return self.map:set(section, self.option, value)
576 function AbstractValue.remove(self, section)
577 return self.map:del(section, self.option)
584 Value - A one-line value
585 maxlength: The maximum length
586 isnumber: The value must be a valid (floating point) number
587 isinteger: The value must be a valid integer
588 ispositive: The value must be positive (and a number)
590 Value = class(AbstractValue)
592 function Value.__init__(self, ...)
593 AbstractValue.__init__(self, ...)
594 self.template = "cbi/value"
597 self.isnumber = false
598 self.isinteger = false
601 -- This validation is a bit more complex
602 function Value.validate(self, val)
603 if self.maxlength and tostring(val):len() > self.maxlength then
607 return ffluci.util.validate(val, self.isnumber, self.isinteger)
611 -- DummyValue - This does nothing except being there
612 DummyValue = class(AbstractValue)
614 function DummyValue.__init__(self, map, ...)
615 AbstractValue.__init__(self, map, ...)
616 self.template = "cbi/dvalue"
620 function DummyValue.parse(self)
624 function DummyValue.render(self, s)
625 ffluci.template.render(self.template, {self=self, section=s})
630 Flag - A flag being enabled or disabled
632 Flag = class(AbstractValue)
634 function Flag.__init__(self, ...)
635 AbstractValue.__init__(self, ...)
636 self.template = "cbi/fvalue"
642 -- A flag can only have two states: set or unset
643 function Flag.parse(self, section)
644 local fvalue = self:formvalue(section)
647 fvalue = self.enabled
649 fvalue = self.disabled
652 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
653 if not(fvalue == self:cfgvalue(section)) then
654 self:write(section, fvalue)
664 ListValue - A one-line value predefined in a list
665 widget: The widget that will be used (select, radio)
667 ListValue = class(AbstractValue)
669 function ListValue.__init__(self, ...)
670 AbstractValue.__init__(self, ...)
671 self.template = "cbi/lvalue"
676 self.widget = "select"
679 function ListValue.value(self, key, val)
681 table.insert(self.keylist, tostring(key))
682 table.insert(self.vallist, tostring(val))
685 function ListValue.validate(self, val)
686 if ffluci.util.contains(self.keylist, val) then
696 MultiValue - Multiple delimited values
697 widget: The widget that will be used (select, checkbox)
698 delimiter: The delimiter that will separate the values (default: " ")
700 MultiValue = class(AbstractValue)
702 function MultiValue.__init__(self, ...)
703 AbstractValue.__init__(self, ...)
704 self.template = "cbi/mvalue"
708 self.widget = "checkbox"
712 function MultiValue.value(self, key, val)
714 table.insert(self.keylist, tostring(key))
715 table.insert(self.vallist, tostring(val))
718 function MultiValue.valuelist(self, section)
719 local val = self:cfgvalue(section)
721 if not(type(val) == "string") then
725 return ffluci.util.split(val, self.delimiter)
728 function MultiValue.validate(self, val)
729 if not(type(val) == "string") then
735 for value in val:gmatch("[^\n]+") do
736 if ffluci.util.contains(self.keylist, value) then
737 result = result .. self.delimiter .. value
741 if result:len() > 0 then
742 return result:sub(self.delimiter:len() + 1)