* Mördercommit ;-)
[project/luci.git] / core / src / 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 require("ffluci.config")
42 require("ffluci.sys")
43
44 local cbidir = ffluci.sys.libpath() .. "/model/cbi/"
45 local func, err = loadfile(cbidir..cbimap..".lua")
46
47 if not func then
48 return nil
49 end
50
51 ffluci.i18n.loadc("cbi")
52
53 ffluci.util.resfenv(func)
54 ffluci.util.updfenv(func, ffluci.cbi)
55 ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
56
57 local map = func()
58
59 if not instanceof(map, Map) then
60 error("CBI map returns no valid map object!")
61 return nil
62 end
63
64 return map
65 end
66
67 -- Node pseudo abstract class
68 Node = class()
69
70 function Node.__init__(self, title, description)
71 self.children = {}
72 self.title = title or ""
73 self.description = description or ""
74 self.template = "cbi/node"
75 end
76
77 -- Append child nodes
78 function Node.append(self, obj)
79 table.insert(self.children, obj)
80 end
81
82 -- Parse this node and its children
83 function Node.parse(self, ...)
84 for k, child in ipairs(self.children) do
85 child:parse(...)
86 end
87 end
88
89 -- Render this node
90 function Node.render(self)
91 ffluci.template.render(self.template, {self=self})
92 end
93
94 -- Render the children
95 function Node.render_children(self, ...)
96 for k, node in ipairs(self.children) do
97 node:render(...)
98 end
99 end
100
101
102 --[[
103 A simple template element
104 ]]--
105 Template = class(Node)
106
107 function Template.__init__(self, template)
108 Node.__init__(self)
109 self.template = template
110 end
111
112
113 --[[
114 Map - A map describing a configuration file
115 ]]--
116 Map = class(Node)
117
118 function Map.__init__(self, config, ...)
119 Node.__init__(self, ...)
120 self.config = config
121 self.template = "cbi/map"
122 self.uci = ffluci.model.uci.Session()
123 self.ucidata, self.uciorder = self.uci:sections(self.config)
124 if not self.ucidata or not self.uciorder then
125 error("Unable to read UCI data: " .. self.config)
126 end
127 end
128
129 -- Use optimized UCI writing
130 function Map.parse(self, ...)
131 self.uci:t_load(self.config)
132 Node.parse(self, ...)
133 self.uci:t_save(self.config)
134 end
135
136 -- Creates a child section
137 function Map.section(self, class, ...)
138 if instanceof(class, AbstractSection) then
139 local obj = class(self, ...)
140 self:append(obj)
141 return obj
142 else
143 error("class must be a descendent of AbstractSection")
144 end
145 end
146
147 -- UCI add
148 function Map.add(self, sectiontype)
149 local name = self.uci:t_add(self.config, sectiontype)
150 if name then
151 self.ucidata[name] = {}
152 self.ucidata[name][".type"] = sectiontype
153 table.insert(self.uciorder, name)
154 end
155 return name
156 end
157
158 -- UCI set
159 function Map.set(self, section, option, value)
160 local stat = self.uci:t_set(self.config, section, option, value)
161 if stat then
162 local val = self.uci:t_get(self.config, section, option)
163 if option then
164 self.ucidata[section][option] = val
165 else
166 if not self.ucidata[section] then
167 self.ucidata[section] = {}
168 end
169 self.ucidata[section][".type"] = val
170 table.insert(self.uciorder, section)
171 end
172 end
173 return stat
174 end
175
176 -- UCI del
177 function Map.del(self, section, option)
178 local stat = self.uci:t_del(self.config, section, option)
179 if stat then
180 if option then
181 self.ucidata[section][option] = nil
182 else
183 self.ucidata[section] = nil
184 for i, k in ipairs(self.uciorder) do
185 if section == k then
186 table.remove(self.uciorder, i)
187 end
188 end
189 end
190 end
191 return stat
192 end
193
194 -- UCI get (cached)
195 function Map.get(self, section, option)
196 if not section then
197 return self.ucidata, self.uciorder
198 elseif option and self.ucidata[section] then
199 return self.ucidata[section][option]
200 else
201 return self.ucidata[section]
202 end
203 end
204
205
206 --[[
207 AbstractSection
208 ]]--
209 AbstractSection = class(Node)
210
211 function AbstractSection.__init__(self, map, sectiontype, ...)
212 Node.__init__(self, ...)
213 self.sectiontype = sectiontype
214 self.map = map
215 self.config = map.config
216 self.optionals = {}
217
218 self.optional = true
219 self.addremove = false
220 self.dynamic = false
221 end
222
223 -- Appends a new option
224 function AbstractSection.option(self, class, ...)
225 if instanceof(class, AbstractValue) then
226 local obj = class(self.map, ...)
227 self:append(obj)
228 return obj
229 else
230 error("class must be a descendent of AbstractValue")
231 end
232 end
233
234 -- Parse optional options
235 function AbstractSection.parse_optionals(self, section)
236 if not self.optional then
237 return
238 end
239
240 self.optionals[section] = {}
241
242 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
243 for k,v in ipairs(self.children) do
244 if v.optional and not v:cfgvalue(section) then
245 if field == v.option then
246 field = nil
247 else
248 table.insert(self.optionals[section], v)
249 end
250 end
251 end
252
253 if field and #field > 0 and self.dynamic then
254 self:add_dynamic(field)
255 end
256 end
257
258 -- Add a dynamic option
259 function AbstractSection.add_dynamic(self, field, optional)
260 local o = self:option(Value, field, field)
261 o.optional = optional
262 end
263
264 -- Parse all dynamic options
265 function AbstractSection.parse_dynamic(self, section)
266 if not self.dynamic then
267 return
268 end
269
270 local arr = ffluci.util.clone(self:cfgvalue(section))
271 local form = ffluci.http.formvaluetable("cbid."..self.config.."."..section)
272 for k, v in pairs(form) do
273 arr[k] = v
274 end
275
276 for key,val in pairs(arr) do
277 local create = true
278
279 for i,c in ipairs(self.children) do
280 if c.option == key then
281 create = false
282 end
283 end
284
285 if create and key:sub(1, 1) ~= "." then
286 self:add_dynamic(key, true)
287 end
288 end
289 end
290
291 -- Returns the section's UCI table
292 function AbstractSection.cfgvalue(self, section)
293 return self.map:get(section)
294 end
295
296 -- Removes the section
297 function AbstractSection.remove(self, section)
298 return self.map:del(section)
299 end
300
301 -- Creates the section
302 function AbstractSection.create(self, section)
303 return self.map:set(section, nil, self.sectiontype)
304 end
305
306
307
308 --[[
309 NamedSection - A fixed configuration section defined by its name
310 ]]--
311 NamedSection = class(AbstractSection)
312
313 function NamedSection.__init__(self, map, section, ...)
314 AbstractSection.__init__(self, map, ...)
315 self.template = "cbi/nsection"
316
317 self.section = section
318 self.addremove = false
319 end
320
321 function NamedSection.parse(self)
322 local s = self.section
323 local active = self:cfgvalue(s)
324
325
326 if self.addremove then
327 local path = self.config.."."..s
328 if active then -- Remove the section
329 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
330 return
331 end
332 else -- Create and apply default values
333 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
334 for k,v in pairs(self.children) do
335 v:write(s, v.default)
336 end
337 end
338 end
339 end
340
341 if active then
342 AbstractSection.parse_dynamic(self, s)
343 if ffluci.http.formvalue("cbi.submit") then
344 Node.parse(self, s)
345 end
346 AbstractSection.parse_optionals(self, s)
347 end
348 end
349
350
351 --[[
352 TypedSection - A (set of) configuration section(s) defined by the type
353 addremove: Defines whether the user can add/remove sections of this type
354 anonymous: Allow creating anonymous sections
355 validate: a validation function returning nil if the section is invalid
356 ]]--
357 TypedSection = class(AbstractSection)
358
359 function TypedSection.__init__(self, ...)
360 AbstractSection.__init__(self, ...)
361 self.template = "cbi/tsection"
362 self.deps = {}
363 self.excludes = {}
364
365 self.anonymous = false
366 end
367
368 -- Return all matching UCI sections for this TypedSection
369 function TypedSection.cfgsections(self)
370 local sections = {}
371 local map, order = self.map:get()
372
373 for i, k in ipairs(order) do
374 if map[k][".type"] == self.sectiontype then
375 if self:checkscope(k) then
376 table.insert(sections, k)
377 end
378 end
379 end
380
381 return sections
382 end
383
384 -- Creates a new section of this type with the given name (or anonymous)
385 function TypedSection.create(self, name)
386 if name then
387 self.map:set(name, nil, self.sectiontype)
388 else
389 name = self.map:add(self.sectiontype)
390 end
391
392 for k,v in pairs(self.children) do
393 if v.default then
394 self.map:set(name, v.option, v.default)
395 end
396 end
397 end
398
399 -- Limits scope to sections that have certain option => value pairs
400 function TypedSection.depends(self, option, value)
401 table.insert(self.deps, {option=option, value=value})
402 end
403
404 -- Excludes several sections by name
405 function TypedSection.exclude(self, field)
406 self.excludes[field] = true
407 end
408
409 function TypedSection.parse(self)
410 if self.addremove then
411 -- Create
412 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
413 local name = ffluci.http.formvalue(crval)
414 if self.anonymous then
415 if name then
416 self:create()
417 end
418 else
419 if name then
420 -- Ignore if it already exists
421 if self:cfgvalue(name) then
422 name = nil;
423 end
424
425 name = self:checkscope(name)
426
427 if not name then
428 self.err_invalid = true
429 end
430
431 if name and name:len() > 0 then
432 self:create(name)
433 end
434 end
435 end
436
437 -- Remove
438 crval = "cbi.rts." .. self.config
439 name = ffluci.http.formvaluetable(crval)
440 for k,v in pairs(name) do
441 if self:cfgvalue(k) and self:checkscope(k) then
442 self:remove(k)
443 end
444 end
445 end
446
447 for i, k in ipairs(self:cfgsections()) do
448 AbstractSection.parse_dynamic(self, k)
449 if ffluci.http.formvalue("cbi.submit") then
450 Node.parse(self, k)
451 end
452 AbstractSection.parse_optionals(self, k)
453 end
454 end
455
456 -- Render the children
457 function TypedSection.render_children(self, section)
458 for k, node in ipairs(self.children) do
459 node:render(section)
460 end
461 end
462
463 -- Verifies scope of sections
464 function TypedSection.checkscope(self, section)
465 -- Check if we are not excluded
466 if self.excludes[section] then
467 return nil
468 end
469
470 -- Check if at least one dependency is met
471 if #self.deps > 0 and self:cfgvalue(section) then
472 local stat = false
473
474 for k, v in ipairs(self.deps) do
475 if self:cfgvalue(section)[v.option] == v.value then
476 stat = true
477 end
478 end
479
480 if not stat then
481 return nil
482 end
483 end
484
485 return self:validate(section)
486 end
487
488
489 -- Dummy validate function
490 function TypedSection.validate(self, section)
491 return section
492 end
493
494
495 --[[
496 AbstractValue - An abstract Value Type
497 null: Value can be empty
498 valid: A function returning the value if it is valid otherwise nil
499 depends: A table of option => value pairs of which one must be true
500 default: The default value
501 size: The size of the input fields
502 rmempty: Unset value if empty
503 optional: This value is optional (see AbstractSection.optionals)
504 ]]--
505 AbstractValue = class(Node)
506
507 function AbstractValue.__init__(self, map, option, ...)
508 Node.__init__(self, ...)
509 self.option = option
510 self.map = map
511 self.config = map.config
512 self.tag_invalid = {}
513 self.deps = {}
514
515 self.rmempty = false
516 self.default = nil
517 self.size = nil
518 self.optional = false
519 end
520
521 -- Add a dependencie to another section field
522 function AbstractValue.depends(self, field, value)
523 table.insert(self.deps, {field=field, value=value})
524 end
525
526 -- Return whether this object should be created
527 function AbstractValue.formcreated(self, section)
528 local key = "cbi.opt."..self.config.."."..section
529 return (ffluci.http.formvalue(key) == self.option)
530 end
531
532 -- Returns the formvalue for this object
533 function AbstractValue.formvalue(self, section)
534 local key = "cbid."..self.map.config.."."..section.."."..self.option
535 return ffluci.http.formvalue(key)
536 end
537
538 function AbstractValue.parse(self, section)
539 local fvalue = self:formvalue(section)
540
541 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
542 fvalue = self:validate(fvalue)
543 if not fvalue then
544 self.tag_invalid[section] = true
545 end
546 if fvalue and not (fvalue == self:cfgvalue(section)) then
547 self:write(section, fvalue)
548 end
549 else -- Unset the UCI or error
550 if self.rmempty or self.optional then
551 self:remove(section)
552 end
553 end
554 end
555
556 -- Render if this value exists or if it is mandatory
557 function AbstractValue.render(self, s)
558 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
559 ffluci.template.render(self.template, {self=self, section=s})
560 end
561 end
562
563 -- Return the UCI value of this object
564 function AbstractValue.cfgvalue(self, section)
565 return self.map:get(section, self.option)
566 end
567
568 -- Validate the form value
569 function AbstractValue.validate(self, value)
570 return value
571 end
572
573 -- Write to UCI
574 function AbstractValue.write(self, section, value)
575 return self.map:set(section, self.option, value)
576 end
577
578 -- Remove from UCI
579 function AbstractValue.remove(self, section)
580 return self.map:del(section, self.option)
581 end
582
583
584
585
586 --[[
587 Value - A one-line value
588 maxlength: The maximum length
589 isnumber: The value must be a valid (floating point) number
590 isinteger: The value must be a valid integer
591 ispositive: The value must be positive (and a number)
592 ]]--
593 Value = class(AbstractValue)
594
595 function Value.__init__(self, ...)
596 AbstractValue.__init__(self, ...)
597 self.template = "cbi/value"
598
599 self.maxlength = nil
600 self.isnumber = false
601 self.isinteger = false
602 end
603
604 -- This validation is a bit more complex
605 function Value.validate(self, val)
606 if self.maxlength and tostring(val):len() > self.maxlength then
607 val = nil
608 end
609
610 return ffluci.util.validate(val, self.isnumber, self.isinteger)
611 end
612
613
614 -- DummyValue - This does nothing except being there
615 DummyValue = class(AbstractValue)
616
617 function DummyValue.__init__(self, map, ...)
618 AbstractValue.__init__(self, map, ...)
619 self.template = "cbi/dvalue"
620 self.value = nil
621 end
622
623 function DummyValue.parse(self)
624
625 end
626
627 function DummyValue.render(self, s)
628 ffluci.template.render(self.template, {self=self, section=s})
629 end
630
631
632 --[[
633 Flag - A flag being enabled or disabled
634 ]]--
635 Flag = class(AbstractValue)
636
637 function Flag.__init__(self, ...)
638 AbstractValue.__init__(self, ...)
639 self.template = "cbi/fvalue"
640
641 self.enabled = "1"
642 self.disabled = "0"
643 end
644
645 -- A flag can only have two states: set or unset
646 function Flag.parse(self, section)
647 local fvalue = self:formvalue(section)
648
649 if fvalue then
650 fvalue = self.enabled
651 else
652 fvalue = self.disabled
653 end
654
655 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
656 if not(fvalue == self:cfgvalue(section)) then
657 self:write(section, fvalue)
658 end
659 else
660 self:remove(section)
661 end
662 end
663
664
665
666 --[[
667 ListValue - A one-line value predefined in a list
668 widget: The widget that will be used (select, radio)
669 ]]--
670 ListValue = class(AbstractValue)
671
672 function ListValue.__init__(self, ...)
673 AbstractValue.__init__(self, ...)
674 self.template = "cbi/lvalue"
675 self.keylist = {}
676 self.vallist = {}
677
678 self.size = 1
679 self.widget = "select"
680 end
681
682 function ListValue.value(self, key, val)
683 val = val or key
684 table.insert(self.keylist, tostring(key))
685 table.insert(self.vallist, tostring(val))
686 end
687
688 function ListValue.validate(self, val)
689 if ffluci.util.contains(self.keylist, val) then
690 return val
691 else
692 return nil
693 end
694 end
695
696
697
698 --[[
699 MultiValue - Multiple delimited values
700 widget: The widget that will be used (select, checkbox)
701 delimiter: The delimiter that will separate the values (default: " ")
702 ]]--
703 MultiValue = class(AbstractValue)
704
705 function MultiValue.__init__(self, ...)
706 AbstractValue.__init__(self, ...)
707 self.template = "cbi/mvalue"
708 self.keylist = {}
709 self.vallist = {}
710
711 self.widget = "checkbox"
712 self.delimiter = " "
713 end
714
715 function MultiValue.value(self, key, val)
716 val = val or key
717 table.insert(self.keylist, tostring(key))
718 table.insert(self.vallist, tostring(val))
719 end
720
721 function MultiValue.valuelist(self, section)
722 local val = self:cfgvalue(section)
723
724 if not(type(val) == "string") then
725 return {}
726 end
727
728 return ffluci.util.split(val, self.delimiter)
729 end
730
731 function MultiValue.validate(self, val)
732 if not(type(val) == "string") then
733 return nil
734 end
735
736 local result = ""
737
738 for value in val:gmatch("[^\n]+") do
739 if ffluci.util.contains(self.keylist, value) then
740 result = result .. self.delimiter .. value
741 end
742 end
743
744 if result:len() > 0 then
745 return result:sub(self.delimiter:len() + 1)
746 else
747 return nil
748 end
749 end