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