* Major CBI improvements
[project/luci.git] / src / ffluci / cbi.lua
1 --[[
2 FFLuCI - Configuration Bind Interface
3
4 Description:
5 Offers an interface for binding confiugration values to certain
6 data types. Supports value and range validation and basic dependencies.
7
8 FileId:
9 $Id$
10
11 License:
12 Copyright 2008 Steven Barth <steven@midlink.org>
13
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
17
18 http://www.apache.org/licenses/LICENSE-2.0
19
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.
25
26 ]]--
27 module("ffluci.cbi", package.seeall)
28
29 require("ffluci.template")
30 require("ffluci.util")
31 require("ffluci.http")
32 require("ffluci.model.uci")
33
34 local class = ffluci.util.class
35 local instanceof = ffluci.util.instanceof
36
37 -- Loads a CBI map from given file, creating an environment and returns it
38 function load(cbimap)
39 require("ffluci.fs")
40 require("ffluci.i18n")
41
42 local cbidir = ffluci.fs.dirname(ffluci.util.__file__()) .. "model/cbi/"
43 local func, err = loadfile(cbidir..cbimap..".lua")
44
45 if not func then
46 error(err)
47 return nil
48 end
49
50 ffluci.util.resfenv(func)
51 ffluci.util.updfenv(func, ffluci.cbi)
52 ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
53
54 local map = func()
55
56 if not instanceof(map, Map) then
57 error("CBI map returns no valid map object!")
58 return nil
59 end
60
61 ffluci.i18n.loadc("cbi")
62
63 return map
64 end
65
66 -- Node pseudo abstract class
67 Node = class()
68
69 function Node.__init__(self, title, description)
70 self.children = {}
71 self.title = title or ""
72 self.description = description or ""
73 self.template = "cbi/node"
74 end
75
76 -- Append child nodes
77 function Node.append(self, obj)
78 table.insert(self.children, obj)
79 end
80
81 -- Parse this node and its children
82 function Node.parse(self, ...)
83 for k, child in ipairs(self.children) do
84 child:parse(...)
85 end
86 end
87
88 -- Render this node
89 function Node.render(self)
90 ffluci.template.render(self.template, {self=self})
91 end
92
93 -- Render the children
94 function Node.render_children(self, ...)
95 for k, node in ipairs(self.children) do
96 node:render(...)
97 end
98 end
99
100
101 --[[
102 Map - A map describing a configuration file
103 ]]--
104 Map = class(Node)
105
106 function Map.__init__(self, config, ...)
107 Node.__init__(self, ...)
108 self.config = config
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)
114 else
115 self.ucidata = self.ucidata[self.config]
116 end
117 end
118
119 -- Creates a child section
120 function Map.section(self, class, ...)
121 if instanceof(class, AbstractSection) then
122 local obj = class(self, ...)
123 self:append(obj)
124 return obj
125 else
126 error("class must be a descendent of AbstractSection")
127 end
128 end
129
130 -- UCI add
131 function Map.add(self, sectiontype)
132 local name = self.uci:add(self.config, sectiontype)
133 if name then
134 self.ucidata[name] = self.uci:show(self.config, name)
135 end
136 return name
137 end
138
139 -- UCI set
140 function Map.set(self, section, option, value)
141 local stat = self.uci:set(self.config, section, option, value)
142 if stat then
143 local val = self.uci:get(self.config, section, option)
144 if option then
145 self.ucidata[section][option] = val
146 else
147 if not self.ucidata[section] then
148 self.ucidata[section] = {}
149 end
150 self.ucidata[section][".type"] = val
151 end
152 end
153 return stat
154 end
155
156 -- UCI del
157 function Map.del(self, section, option)
158 local stat = self.uci:del(self.config, section, option)
159 if stat then
160 if option then
161 self.ucidata[section][option] = nil
162 else
163 self.ucidata[section] = nil
164 end
165 end
166 return stat
167 end
168
169 -- UCI get (cached)
170 function Map.get(self, section, option)
171 if not section then
172 return self.ucidata
173 elseif option and self.ucidata[section] then
174 return self.ucidata[section][option]
175 else
176 return self.ucidata[section]
177 end
178 end
179
180
181 --[[
182 AbstractSection
183 ]]--
184 AbstractSection = class(Node)
185
186 function AbstractSection.__init__(self, map, sectiontype, ...)
187 Node.__init__(self, ...)
188 self.sectiontype = sectiontype
189 self.map = map
190 self.config = map.config
191 self.optionals = {}
192
193 self.optional = true
194 self.addremove = false
195 self.dynamic = false
196 end
197
198 -- Appends a new option
199 function AbstractSection.option(self, class, ...)
200 if instanceof(class, AbstractValue) then
201 local obj = class(self.map, ...)
202 self:append(obj)
203 return obj
204 else
205 error("class must be a descendent of AbstractValue")
206 end
207 end
208
209 -- Parse optional options
210 function AbstractSection.parse_optionals(self, section)
211 if not self.optional then
212 return
213 end
214
215 self.optionals[section] = {}
216
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
221 field = nil
222 else
223 table.insert(self.optionals[section], v)
224 end
225 end
226 end
227
228 if field and field:len() > 0 and self.dynamic then
229 self:add_dynamic(field)
230 end
231 end
232
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
237 end
238
239 -- Parse all dynamic options
240 function AbstractSection.parse_dynamic(self, section)
241 if not self.dynamic then
242 return
243 end
244
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
249 arr[k] = v
250 end
251 end
252
253 for key,val in pairs(arr) do
254 local create = true
255
256 for i,c in ipairs(self.children) do
257 if c.option == key then
258 create = false
259 end
260 end
261
262 if create and key:sub(1, 1) ~= "." then
263 self:add_dynamic(key, true)
264 end
265 end
266 end
267
268 -- Returns the section's UCI table
269 function AbstractSection.cfgvalue(self, section)
270 return self.map:get(section)
271 end
272
273 -- Removes the section
274 function AbstractSection.remove(self, section)
275 return self.map:del(section)
276 end
277
278 -- Creates the section
279 function AbstractSection.create(self, section)
280 return self.map:set(section, nil, self.sectiontype)
281 end
282
283
284
285 --[[
286 NamedSection - A fixed configuration section defined by its name
287 ]]--
288 NamedSection = class(AbstractSection)
289
290 function NamedSection.__init__(self, map, section, ...)
291 AbstractSection.__init__(self, map, ...)
292 self.template = "cbi/nsection"
293
294 self.section = section
295 self.addremove = false
296 end
297
298 function NamedSection.parse(self)
299 local s = self.section
300 local active = self:cfgvalue(s)
301
302
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
307 return
308 end
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)
313 end
314 end
315 end
316 end
317
318 if active then
319 AbstractSection.parse_dynamic(self, s)
320 Node.parse(self, s)
321 AbstractSection.parse_optionals(self, s)
322 end
323 end
324
325
326 --[[
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
331 ]]--
332 TypedSection = class(AbstractSection)
333
334 function TypedSection.__init__(self, ...)
335 AbstractSection.__init__(self, ...)
336 self.template = "cbi/tsection"
337 self.deps = {}
338 self.excludes = {}
339
340 self.anonymous = false
341 end
342
343 -- Return all matching UCI sections for this TypedSection
344 function TypedSection.cfgsections(self)
345 local sections = {}
346 for k, v in pairs(self.map:get()) do
347 if v[".type"] == self.sectiontype then
348 if self:checkscope(k) then
349 sections[k] = v
350 end
351 end
352 end
353 return sections
354 end
355
356 -- Creates a new section of this type with the given name (or anonymous)
357 function TypedSection.create(self, name)
358 if name then
359 self.map:set(name, nil, self.sectiontype)
360 else
361 name = self.map:add(self.sectiontype)
362 end
363
364 for k,v in pairs(self.children) do
365 if v.default then
366 self.map:set(name, v.option, v.default)
367 end
368 end
369 end
370
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})
374 end
375
376 -- Excludes several sections by name
377 function TypedSection.exclude(self, field)
378 self.excludes[field] = true
379 end
380
381 function TypedSection.parse(self)
382 if self.addremove then
383 -- Create
384 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
385 local name = ffluci.http.formvalue(crval)
386 if self.anonymous then
387 if name then
388 self:create()
389 end
390 else
391 if name then
392 -- Ignore if it already exists
393 if self:cfgvalue(name) then
394 name = nil;
395 end
396
397 name = self:checkscope(name)
398
399 if not name then
400 self.err_invalid = true
401 end
402
403 if name and name:len() > 0 then
404 self:create(name)
405 end
406 end
407 end
408
409 -- Remove
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
415 self:remove(k)
416 end
417 end
418 end
419 end
420
421 for k, v in pairs(self:cfgsections()) do
422 AbstractSection.parse_dynamic(self, k)
423 Node.parse(self, k)
424 AbstractSection.parse_optionals(self, k)
425 end
426 end
427
428 -- Render the children
429 function TypedSection.render_children(self, section)
430 for k, node in ipairs(self.children) do
431 node:render(section)
432 end
433 end
434
435 -- Verifies scope of sections
436 function TypedSection.checkscope(self, section)
437 -- Check if we are not excluded
438 if self.excludes[section] then
439 return nil
440 end
441
442 -- Check if at least one dependency is met
443 if #self.deps > 0 and self:cfgvalue(section) then
444 local stat = false
445
446 for k, v in ipairs(self.deps) do
447 if self:cfgvalue(section)[v.option] == v.value then
448 stat = true
449 end
450 end
451
452 if not stat then
453 return nil
454 end
455 end
456
457 return self:validate(section)
458 end
459
460
461 -- Dummy validate function
462 function TypedSection.validate(self, section)
463 return section
464 end
465
466
467 --[[
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)
476 ]]--
477 AbstractValue = class(Node)
478
479 function AbstractValue.__init__(self, map, option, ...)
480 Node.__init__(self, ...)
481 self.option = option
482 self.map = map
483 self.config = map.config
484 self.tag_invalid = {}
485 self.deps = {}
486
487 self.rmempty = false
488 self.default = nil
489 self.size = nil
490 self.optional = false
491 end
492
493 -- Add a dependencie to another section field
494 function AbstractValue.depends(self, field, value)
495 table.insert(self.deps, {field=field, value=value})
496 end
497
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)
502 end
503
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)
508 end
509
510 function AbstractValue.parse(self, section)
511 local fvalue = self:formvalue(section)
512
513 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
514 fvalue = self:validate(fvalue)
515 if not fvalue then
516 self.tag_invalid[section] = true
517 end
518 if fvalue and not (fvalue == self:cfgvalue(section)) then
519 self:write(section, fvalue)
520 end
521 elseif ffluci.http.formvalue("cbi.submit") then -- Unset the UCI or error
522 if self.rmempty or self.optional then
523 self:remove(section)
524 end
525 end
526 end
527
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})
532 end
533 end
534
535 -- Return the UCI value of this object
536 function AbstractValue.cfgvalue(self, section)
537 return self.map:get(section, self.option)
538 end
539
540 -- Validate the form value
541 function AbstractValue.validate(self, value)
542 return value
543 end
544
545 -- Write to UCI
546 function AbstractValue.write(self, section, value)
547 return self.map:set(section, self.option, value)
548 end
549
550 -- Remove from UCI
551 function AbstractValue.remove(self, section)
552 return self.map:del(section, self.option)
553 end
554
555
556
557
558 --[[
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)
564 ]]--
565 Value = class(AbstractValue)
566
567 function Value.__init__(self, ...)
568 AbstractValue.__init__(self, ...)
569 self.template = "cbi/value"
570
571 self.maxlength = nil
572 self.isnumber = false
573 self.isinteger = false
574 end
575
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
579 val = nil
580 end
581
582 return ffluci.util.validate(val, self.isnumber, self.isinteger)
583 end
584
585
586
587 --[[
588 Flag - A flag being enabled or disabled
589 ]]--
590 Flag = class(AbstractValue)
591
592 function Flag.__init__(self, ...)
593 AbstractValue.__init__(self, ...)
594 self.template = "cbi/fvalue"
595
596 self.enabled = "1"
597 self.disabled = "0"
598 end
599
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)
604
605 if fvalue then
606 fvalue = self.enabled
607 else
608 fvalue = self.disabled
609 end
610
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)
614 end
615 else
616 self:remove(section)
617 end
618 end
619
620
621
622 --[[
623 ListValue - A one-line value predefined in a list
624 widget: The widget that will be used (select, radio)
625 ]]--
626 ListValue = class(AbstractValue)
627
628 function ListValue.__init__(self, ...)
629 AbstractValue.__init__(self, ...)
630 self.template = "cbi/lvalue"
631 self.keylist = {}
632 self.vallist = {}
633
634 self.size = 1
635 self.widget = "select"
636 end
637
638 function ListValue.value(self, key, val)
639 val = val or key
640 table.insert(self.keylist, tostring(key))
641 table.insert(self.vallist, tostring(val))
642 end
643
644 function ListValue.validate(self, val)
645 if ffluci.util.contains(self.keylist, val) then
646 return val
647 else
648 return nil
649 end
650 end
651
652
653
654 --[[
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: " ")
658 ]]--
659 MultiValue = class(AbstractValue)
660
661 function MultiValue.__init__(self, ...)
662 AbstractValue.__init__(self, ...)
663 self.template = "cbi/mvalue"
664 self.keylist = {}
665 self.vallist = {}
666
667 self.widget = "checkbox"
668 self.delimiter = " "
669 end
670
671 function MultiValue.value(self, key, val)
672 val = val or key
673 table.insert(self.keylist, tostring(key))
674 table.insert(self.vallist, tostring(val))
675 end
676
677 function MultiValue.valuelist(self, section)
678 local val = self:cfgvalue(section)
679
680 if not(type(val) == "string") then
681 return {}
682 end
683
684 return ffluci.util.split(val, self.delimiter)
685 end
686
687 function MultiValue.validate(self, val)
688 if not(type(val) == "string") then
689 return nil
690 end
691
692 local result = ""
693
694 for value in val:gmatch("[^\n]+") do
695 if ffluci.util.contains(self.keylist, value) then
696 result = result .. self.delimiter .. value
697 end
698 end
699
700 if result:len() > 0 then
701 return result:sub(self.delimiter:len() + 1)
702 else
703 return nil
704 end
705 end