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