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