* CBI update
[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 error(err)
47 return nil
48 end
49
50 ffluci.util.resfenv(func)
51 ffluci.util.updfenv(func, ffluci.cbi)
52 ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
53
54 local map = func()
55
56 if not instanceof(map, Map) then
57 error("CBI map returns no valid map object!")
58 return nil
59 end
60
61 ffluci.i18n.loadc("cbi")
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 self.ucidata = self.ucidata[self.config]
116 end
117 end
118
119 -- Creates a child section
120 function Map.section(self, class, ...)
121 if instanceof(class, AbstractSection) then
122 local obj = class(self, ...)
123 self:append(obj)
124 return obj
125 else
126 error("class must be a descendent of AbstractSection")
127 end
128 end
129
130 -- UCI add
131 function Map.add(self, sectiontype)
132 local name = self.uci:add(self.config, sectiontype)
133 if name then
134 self.ucidata[name] = self.uci:show(self.config, name)
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 self.map:set(section, field, v.default)
222 field = nil
223 else
224 table.insert(self.optionals[section], v)
225 end
226 end
227 end
228
229 if field and field:len() > 0 and self.dynamic then
230 self:add_dynamic(field)
231 end
232 end
233
234 -- Add a dynamic option
235 function AbstractSection.add_dynamic(self, field, optional)
236 local o = self:option(Value, field, field)
237 o.optional = optional
238 end
239
240 -- Parse all dynamic options
241 function AbstractSection.parse_dynamic(self, section)
242 if not self.dynamic then
243 return
244 end
245
246 local arr = ffluci.util.clone(self:cfgvalue(section))
247 local form = ffluci.http.formvalue("cbid."..self.config.."."..section)
248 if type(form) == "table" then
249 for k,v in pairs(form) do
250 arr[k] = v
251 end
252 end
253
254 for key,val in pairs(arr) do
255 local create = true
256
257 for i,c in ipairs(self.children) do
258 if c.option == key then
259 create = false
260 end
261 end
262
263 if create and key:sub(1, 1) ~= "." then
264 self:add_dynamic(key, true)
265 end
266 end
267 end
268
269 -- Returns the section's UCI table
270 function AbstractSection.cfgvalue(self, section)
271 return self.map:get(section)
272 end
273
274 -- Removes the section
275 function AbstractSection.remove(self, section)
276 return self.map:del(section)
277 end
278
279 -- Creates the section
280 function AbstractSection.create(self, section)
281 return self.map:set(section, nil, self.sectiontype)
282 end
283
284
285
286 --[[
287 NamedSection - A fixed configuration section defined by its name
288 ]]--
289 NamedSection = class(AbstractSection)
290
291 function NamedSection.__init__(self, map, section, ...)
292 AbstractSection.__init__(self, map, ...)
293 self.template = "cbi/nsection"
294
295 self.section = section
296 self.addremove = false
297 end
298
299 function NamedSection.parse(self)
300 local s = self.section
301 local active = self:cfgvalue(s)
302
303
304 if self.addremove then
305 local path = self.config.."."..s
306 if active then -- Remove the section
307 if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then
308 return
309 end
310 else -- Create and apply default values
311 if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then
312 for k,v in pairs(self.children) do
313 v:write(s, v.default)
314 end
315 end
316 end
317 end
318
319 if active then
320 AbstractSection.parse_dynamic(self, s)
321 Node.parse(self, s)
322 AbstractSection.parse_optionals(self, s)
323 end
324 end
325
326
327 --[[
328 TypedSection - A (set of) configuration section(s) defined by the type
329 addremove: Defines whether the user can add/remove sections of this type
330 anonymous: Allow creating anonymous sections
331 valid: a list of names or a validation function for creating sections
332 scope: a list of names or a validation function for editing sections
333 ]]--
334 TypedSection = class(AbstractSection)
335
336 function TypedSection.__init__(self, ...)
337 AbstractSection.__init__(self, ...)
338 self.template = "cbi/tsection"
339
340 self.anonymous = false
341 self.valid = nil
342 self.scope = nil
343 end
344
345 -- Creates a new section of this type with the given name (or anonymous)
346 function TypedSection.create(self, name)
347 if name then
348 self.map:set(name, nil, self.sectiontype)
349 else
350 name = self.map:add(self.sectiontype)
351 end
352
353 for k,v in pairs(self.children) do
354 if v.default then
355 self.map:set(name, v.option, v.default)
356 end
357 end
358 end
359
360 function TypedSection.parse(self)
361 if self.addremove then
362 -- Create
363 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
364 local name = ffluci.http.formvalue(crval)
365 if self.anonymous then
366 if name then
367 self:create()
368 end
369 else
370 if name then
371 name = ffluci.util.validate(name, self.valid)
372 if not name then
373 self.err_invalid = true
374 end
375 if name and name:len() > 0 then
376 self:create(name)
377 end
378 end
379 end
380
381 -- Remove
382 crval = "cbi.rts." .. self.config
383 name = ffluci.http.formvalue(crval)
384 if type(name) == "table" then
385 for k,v in pairs(name) do
386 if ffluci.util.validate(k, self.valid) then
387 self:remove(k)
388 end
389 end
390 end
391 end
392
393 for k, v in pairs(self:cfgsections()) do
394 AbstractSection.parse_dynamic(self, k)
395 Node.parse(self, k)
396 AbstractSection.parse_optionals(self, k)
397 end
398 end
399
400 -- Render the children
401 function TypedSection.render_children(self, section)
402 for k, node in ipairs(self.children) do
403 node:render(section)
404 end
405 end
406
407 -- Return all matching UCI sections for this TypedSection
408 function TypedSection.cfgsections(self)
409 local sections = {}
410 for k, v in pairs(self.map:get()) do
411 if v[".type"] == self.sectiontype then
412 if ffluci.util.validate(k, self.scope) then
413 sections[k] = v
414 end
415 end
416 end
417 return sections
418 end
419
420
421
422 --[[
423 AbstractValue - An abstract Value Type
424 null: Value can be empty
425 valid: A function returning the value if it is valid otherwise nil
426 depends: A table of option => value pairs of which one must be true
427 default: The default value
428 size: The size of the input fields
429 rmempty: Unset value if empty
430 optional: This value is optional (see AbstractSection.optionals)
431 ]]--
432 AbstractValue = class(Node)
433
434 function AbstractValue.__init__(self, map, option, ...)
435 Node.__init__(self, ...)
436 self.option = option
437 self.map = map
438 self.config = map.config
439 self.tag_invalid = {}
440
441 self.valid = nil
442 self.depends = nil
443 self.default = " "
444 self.size = nil
445 self.optional = false
446 end
447
448 -- Returns the formvalue for this object
449 function AbstractValue.formvalue(self, section)
450 local key = "cbid."..self.map.config.."."..section.."."..self.option
451 return ffluci.http.formvalue(key)
452 end
453
454 function AbstractValue.parse(self, section)
455 local fvalue = self:formvalue(section)
456 if fvalue == "" then
457 fvalue = nil
458 end
459
460
461 if fvalue then -- If we have a form value, validate it and write it to UCI
462 fvalue = self:validate(fvalue)
463 if not fvalue then
464 self.tag_invalid[section] = true
465 end
466 if fvalue and not (fvalue == self:cfgvalue(section)) then
467 self:write(section, fvalue)
468 end
469 elseif ffluci.http.formvalue("cbi.submit") then -- Unset the UCI or error
470 if self.rmempty or self.optional then
471 self:remove(section)
472 else
473 self.tag_invalid[section] = true
474 end
475 end
476 end
477
478 -- Render if this value exists or if it is mandatory
479 function AbstractValue.render(self, section)
480 if not self.optional or self:cfgvalue(section) then
481 ffluci.template.render(self.template, {self=self, section=section})
482 end
483 end
484
485 -- Return the UCI value of this object
486 function AbstractValue.cfgvalue(self, section)
487 return self.map:get(section, self.option)
488 end
489
490 -- Validate the form value
491 function AbstractValue.validate(self, val)
492 return ffluci.util.validate(val, self.valid)
493 end
494
495 -- Write to UCI
496 function AbstractValue.write(self, section, value)
497 return self.map:set(section, self.option, value)
498 end
499
500 -- Remove from UCI
501 function AbstractValue.remove(self, section)
502 return self.map:del(section, self.option)
503 end
504
505
506
507
508 --[[
509 Value - A one-line value
510 maxlength: The maximum length
511 isnumber: The value must be a valid (floating point) number
512 isinteger: The value must be a valid integer
513 ispositive: The value must be positive (and a number)
514 ]]--
515 Value = class(AbstractValue)
516
517 function Value.__init__(self, ...)
518 AbstractValue.__init__(self, ...)
519 self.template = "cbi/value"
520
521 self.maxlength = nil
522 self.isnumber = false
523 self.isinteger = false
524 end
525
526 -- This validation is a bit more complex
527 function Value.validate(self, val)
528 if self.maxlength and tostring(val):len() > self.maxlength then
529 val = nil
530 end
531
532 return ffluci.util.validate(val, self.valid, self.isnumber, self.isinteger)
533 end
534
535
536
537 --[[
538 Flag - A flag being enabled or disabled
539 ]]--
540 Flag = class(AbstractValue)
541
542 function Flag.__init__(self, ...)
543 AbstractValue.__init__(self, ...)
544 self.template = "cbi/fvalue"
545
546 self.enabled = "1"
547 self.disabled = "0"
548 end
549
550 -- A flag can only have two states: set or unset
551 function Flag.parse(self, section)
552 self.default = self.enabled
553 local fvalue = self:formvalue(section)
554
555 if fvalue then
556 fvalue = self.enabled
557 else
558 fvalue = self.disabled
559 end
560
561 if fvalue == self.enabled or (not self.optional and not self.rmempty) then
562 if not(fvalue == self:cfgvalue(section)) then
563 self:write(section, fvalue)
564 end
565 else
566 self:remove(section)
567 end
568 end
569
570
571
572 --[[
573 ListValue - A one-line value predefined in a list
574 widget: The widget that will be used (select, radio)
575 ]]--
576 ListValue = class(AbstractValue)
577
578 function ListValue.__init__(self, ...)
579 AbstractValue.__init__(self, ...)
580 self.template = "cbi/lvalue"
581 self.keylist = {}
582 self.vallist = {}
583
584 self.size = 1
585 self.widget = "select"
586 end
587
588 function ListValue.add_value(self, key, val)
589 val = val or key
590 table.insert(self.keylist, tostring(key))
591 table.insert(self.vallist, tostring(val))
592 end
593
594 function ListValue.validate(self, val)
595 if ffluci.util.contains(self.keylist, val) then
596 return val
597 else
598 return nil
599 end
600 end
601
602
603
604 --[[
605 MultiValue - Multiple delimited values
606 widget: The widget that will be used (select, checkbox)
607 delimiter: The delimiter that will separate the values (default: " ")
608 ]]--
609 MultiValue = class(AbstractValue)
610
611 function MultiValue.__init__(self, ...)
612 AbstractValue.__init__(self, ...)
613 self.template = "cbi/mvalue"
614 self.keylist = {}
615 self.vallist = {}
616
617 self.widget = "checkbox"
618 self.delimiter = " "
619 end
620
621 function MultiValue.add_value(self, key, val)
622 val = val or key
623 table.insert(self.keylist, tostring(key))
624 table.insert(self.vallist, tostring(val))
625 end
626
627 function MultiValue.valuelist(self, section)
628 local val = self:cfgvalue(section)
629
630 if not(type(val) == "string") then
631 return {}
632 end
633
634 return ffluci.util.split(val, self.delimiter)
635 end
636
637 function MultiValue.validate(self, val)
638 if not(type(val) == "string") then
639 return nil
640 end
641
642 local result = ""
643
644 for value in val:gmatch("[^\n]+") do
645 if ffluci.util.contains(self.keylist, value) then
646 result = result .. self.delimiter .. value
647 end
648 end
649
650 if result:len() > 0 then
651 return result:sub(self.delimiter:len() + 1)
652 else
653 return nil
654 end
655 end