296a77b493562ff221679693ece07b845d5cb67b
[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 return nil
47 end
48
49 ffluci.i18n.loadc("cbi")
50
51 ffluci.util.resfenv(func)
52 ffluci.util.updfenv(func, ffluci.cbi)
53 ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
54
55 local map = func()
56
57 if not instanceof(map, Map) then
58 error("CBI map returns no valid map object!")
59 return nil
60 end
61
62 return map
63 end
64
65 -- Node pseudo abstract class
66 Node = class()
67
68 function Node.__init__(self, title, description)
69 self.children = {}
70 self.title = title or ""
71 self.description = description or ""
72 self.template = "cbi/node"
73 end
74
75 -- Append child nodes
76 function Node.append(self, obj)
77 table.insert(self.children, obj)
78 end
79
80 -- Parse this node and its children
81 function Node.parse(self, ...)
82 for k, child in ipairs(self.children) do
83 child:parse(...)
84 end
85 end
86
87 -- Render this node
88 function Node.render(self)
89 ffluci.template.render(self.template, {self=self})
90 end
91
92 -- Render the children
93 function Node.render_children(self, ...)
94 for k, node in ipairs(self.children) do
95 node:render(...)
96 end
97 end
98
99
100 --[[
101 Map - A map describing a configuration file
102 ]]--
103 Map = class(Node)
104
105 function Map.__init__(self, config, ...)
106 Node.__init__(self, ...)
107 self.config = config
108 self.template = "cbi/map"
109 self.uci = ffluci.model.uci.Session()
110 self.ucidata = self.uci:show(self.config)
111 if not self.ucidata then
112 error("Unable to read UCI data: " .. self.config)
113 else
114 self.ucidata = self.ucidata[self.config]
115 end
116 end
117
118 -- Creates a child section
119 function Map.section(self, class, ...)
120 if instanceof(class, AbstractSection) then
121 local obj = class(self, ...)
122 self:append(obj)
123 return obj
124 else
125 error("class must be a descendent of AbstractSection")
126 end
127 end
128
129 -- UCI add
130 function Map.add(self, sectiontype)
131 local name = self.uci:add(self.config, sectiontype)
132 if name then
133 self.ucidata[name] = {}
134 self.ucidata[name][".type"] = sectiontype
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 if ffluci.http.formvalue("cbi.submit") then
321 Node.parse(self, s)
322 end
323 AbstractSection.parse_optionals(self, s)
324 end
325 end
326
327
328 --[[
329 TypedSection - A (set of) configuration section(s) defined by the type
330 addremove: Defines whether the user can add/remove sections of this type
331 anonymous: Allow creating anonymous sections
332 validate: a validation function returning nil if the section is invalid
333 ]]--
334 TypedSection = class(AbstractSection)
335
336 function TypedSection.__init__(self, ...)
337 AbstractSection.__init__(self, ...)
338 self.template = "cbi/tsection"
339 self.deps = {}
340 self.excludes = {}
341
342 self.anonymous = false
343 end
344
345 -- Return all matching UCI sections for this TypedSection
346 function TypedSection.cfgsections(self)
347 local sections = {}
348 for k, v in pairs(self.map:get()) do
349 if v[".type"] == self.sectiontype then
350 if self:checkscope(k) then
351 sections[k] = v
352 end
353 end
354 end
355 return sections
356 end
357
358 -- Creates a new section of this type with the given name (or anonymous)
359 function TypedSection.create(self, name)
360 if name then
361 self.map:set(name, nil, self.sectiontype)
362 else
363 name = self.map:add(self.sectiontype)
364 end
365
366 for k,v in pairs(self.children) do
367 if v.default then
368 self.map:set(name, v.option, v.default)
369 end
370 end
371 end
372
373 -- Limits scope to sections that have certain option => value pairs
374 function TypedSection.depends(self, option, value)
375 table.insert(self.deps, {option=option, value=value})
376 end
377
378 -- Excludes several sections by name
379 function TypedSection.exclude(self, field)
380 self.excludes[field] = true
381 end
382
383 function TypedSection.parse(self)
384 if self.addremove then
385 -- Create
386 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
387 local name = ffluci.http.formvalue(crval)
388 if self.anonymous then
389 if name then
390 self:create()
391 end
392 else
393 if name then
394 -- Ignore if it already exists
395 if self:cfgvalue(name) then
396 name = nil;
397 end
398
399 name = self:checkscope(name)
400
401 if not name then
402 self.err_invalid = true
403 end
404
405 if name and name:len() > 0 then
406 self:create(name)
407 end
408 end
409 end
410
411 -- Remove
412 crval = "cbi.rts." .. self.config
413 name = ffluci.http.formvalue(crval)
414 if type(name) == "table" then
415 for k,v in pairs(name) do
416 if self:cfgvalue(k) and self:checkscope(k) then
417 self:remove(k)
418 end
419 end
420 end
421 end
422
423 for k, v in pairs(self:cfgsections()) do
424 AbstractSection.parse_dynamic(self, k)
425 if ffluci.http.formvalue("cbi.submit") then
426 Node.parse(self, k)
427 end
428 AbstractSection.parse_optionals(self, k)
429 end
430 end
431
432 -- Render the children
433 function TypedSection.render_children(self, section)
434 for k, node in ipairs(self.children) do
435 node:render(section)
436 end
437 end
438
439 -- Verifies scope of sections
440 function TypedSection.checkscope(self, section)
441 -- Check if we are not excluded
442 if self.excludes[section] then
443 return nil
444 end
445
446 -- Check if at least one dependency is met
447 if #self.deps > 0 and self:cfgvalue(section) then
448 local stat = false
449
450 for k, v in ipairs(self.deps) do
451 if self:cfgvalue(section)[v.option] == v.value then
452 stat = true
453 end
454 end
455
456 if not stat then
457 return nil
458 end
459 end
460
461 return self:validate(section)
462 end
463
464
465 -- Dummy validate function
466 function TypedSection.validate(self, section)
467 return section
468 end
469
470
471 --[[
472 AbstractValue - An abstract Value Type
473 null: Value can be empty
474 valid: A function returning the value if it is valid otherwise nil
475 depends: A table of option => value pairs of which one must be true
476 default: The default value
477 size: The size of the input fields
478 rmempty: Unset value if empty
479 optional: This value is optional (see AbstractSection.optionals)
480 ]]--
481 AbstractValue = class(Node)
482
483 function AbstractValue.__init__(self, map, option, ...)
484 Node.__init__(self, ...)
485 self.option = option
486 self.map = map
487 self.config = map.config
488 self.tag_invalid = {}
489 self.deps = {}
490
491 self.rmempty = false
492 self.default = nil
493 self.size = nil
494 self.optional = false
495 end
496
497 -- Add a dependencie to another section field
498 function AbstractValue.depends(self, field, value)
499 table.insert(self.deps, {field=field, value=value})
500 end
501
502 -- Return whether this object should be created
503 function AbstractValue.formcreated(self, section)
504 local key = "cbi.opt."..self.config.."."..section
505 return (ffluci.http.formvalue(key) == self.option)
506 end
507
508 -- Returns the formvalue for this object
509 function AbstractValue.formvalue(self, section)
510 local key = "cbid."..self.map.config.."."..section.."."..self.option
511 return ffluci.http.formvalue(key)
512 end
513
514 function AbstractValue.parse(self, section)
515 local fvalue = self:formvalue(section)
516
517 if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
518 fvalue = self:validate(fvalue)
519 if not fvalue then
520 self.tag_invalid[section] = true
521 end
522 if fvalue and not (fvalue == self:cfgvalue(section)) then
523 self:write(section, fvalue)
524 end
525 else -- Unset the UCI or error
526 if self.rmempty or self.optional then
527 self:remove(section)
528 end
529 end
530 end
531
532 -- Render if this value exists or if it is mandatory
533 function AbstractValue.render(self, s)
534 if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
535 ffluci.template.render(self.template, {self=self, section=s})
536 end
537 end
538
539 -- Return the UCI value of this object
540 function AbstractValue.cfgvalue(self, section)
541 return self.map:get(section, self.option)
542 end
543
544 -- Validate the form value
545 function AbstractValue.validate(self, value)
546 return value
547 end
548
549 -- Write to UCI
550 function AbstractValue.write(self, section, value)
551 return self.map:set(section, self.option, value)
552 end
553
554 -- Remove from UCI
555 function AbstractValue.remove(self, section)
556 return self.map:del(section, self.option)
557 end
558
559
560
561
562 --[[
563 Value - A one-line value
564 maxlength: The maximum length
565 isnumber: The value must be a valid (floating point) number
566 isinteger: The value must be a valid integer
567 ispositive: The value must be positive (and a number)
568 ]]--
569 Value = class(AbstractValue)
570
571 function Value.__init__(self, ...)
572 AbstractValue.__init__(self, ...)
573 self.template = "cbi/value"
574
575 self.maxlength = nil
576 self.isnumber = false
577 self.isinteger = false
578 end
579
580 -- This validation is a bit more complex
581 function Value.validate(self, val)
582 if self.maxlength and tostring(val):len() > self.maxlength then
583 val = nil
584 end
585
586 return ffluci.util.validate(val, self.isnumber, self.isinteger)
587 end
588
589
590 -- DummyValue - This does nothing except being there
591 DummyValue = class(AbstractValue)
592
593 function DummyValue.__init__(self, map, ...)
594 AbstractValue.__init__(self, map, ...)
595 self.template = "cbi/dvalue"
596 self.value = nil
597 end
598
599 function DummyValue.parse(self)
600
601 end
602
603 function DummyValue.render(self, s)
604 ffluci.template.render(self.template, {self=self, section=s})
605 end
606
607
608 --[[
609 Flag - A flag being enabled or disabled
610 ]]--
611 Flag = class(AbstractValue)
612
613 function Flag.__init__(self, ...)
614 AbstractValue.__init__(self, ...)
615 self.template = "cbi/fvalue"
616
617 self.enabled = "1"
618 self.disabled = "0"
619 end
620
621 -- A flag can only have two states: set or unset
622 function Flag.parse(self, section)
623 local fvalue = self:formvalue(section)
624
625 if fvalue then
626 fvalue = self.enabled
627 else
628 fvalue = self.disabled
629 end
630
631 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
632 if not(fvalue == self:cfgvalue(section)) then
633 self:write(section, fvalue)
634 end
635 else
636 self:remove(section)
637 end
638 end
639
640
641
642 --[[
643 ListValue - A one-line value predefined in a list
644 widget: The widget that will be used (select, radio)
645 ]]--
646 ListValue = class(AbstractValue)
647
648 function ListValue.__init__(self, ...)
649 AbstractValue.__init__(self, ...)
650 self.template = "cbi/lvalue"
651 self.keylist = {}
652 self.vallist = {}
653
654 self.size = 1
655 self.widget = "select"
656 end
657
658 function ListValue.value(self, key, val)
659 val = val or key
660 table.insert(self.keylist, tostring(key))
661 table.insert(self.vallist, tostring(val))
662 end
663
664 function ListValue.validate(self, val)
665 if ffluci.util.contains(self.keylist, val) then
666 return val
667 else
668 return nil
669 end
670 end
671
672
673
674 --[[
675 MultiValue - Multiple delimited values
676 widget: The widget that will be used (select, checkbox)
677 delimiter: The delimiter that will separate the values (default: " ")
678 ]]--
679 MultiValue = class(AbstractValue)
680
681 function MultiValue.__init__(self, ...)
682 AbstractValue.__init__(self, ...)
683 self.template = "cbi/mvalue"
684 self.keylist = {}
685 self.vallist = {}
686
687 self.widget = "checkbox"
688 self.delimiter = " "
689 end
690
691 function MultiValue.value(self, key, val)
692 val = val or key
693 table.insert(self.keylist, tostring(key))
694 table.insert(self.vallist, tostring(val))
695 end
696
697 function MultiValue.valuelist(self, section)
698 local val = self:cfgvalue(section)
699
700 if not(type(val) == "string") then
701 return {}
702 end
703
704 return ffluci.util.split(val, self.delimiter)
705 end
706
707 function MultiValue.validate(self, val)
708 if not(type(val) == "string") then
709 return nil
710 end
711
712 local result = ""
713
714 for value in val:gmatch("[^\n]+") do
715 if ffluci.util.contains(self.keylist, value) then
716 result = result .. self.delimiter .. value
717 end
718 end
719
720 if result:len() > 0 then
721 return result:sub(self.delimiter:len() + 1)
722 else
723 return nil
724 end
725 end