7588a7fd6da28846d392bf20a7cb19a7f1bdb01b
[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 option and self.ucidata[section] then
172 return self.ucidata[section][option]
173 else
174 return self.ucidata[section]
175 end
176 end
177
178
179 --[[
180 AbstractSection
181 ]]--
182 AbstractSection = class(Node)
183
184 function AbstractSection.__init__(self, map, sectiontype, ...)
185 Node.__init__(self, ...)
186 self.sectiontype = sectiontype
187 self.map = map
188 self.config = map.config
189 self.optionals = {}
190
191 self.addremove = true
192 self.optional = true
193 self.dynamic = false
194 end
195
196 -- Appends a new option
197 function AbstractSection.option(self, class, ...)
198 if instanceof(class, AbstractValue) then
199 local obj = class(self.map, ...)
200 self:append(obj)
201 return obj
202 else
203 error("class must be a descendent of AbstractValue")
204 end
205 end
206
207 -- Parse optional options
208 function AbstractSection.parse_optionals(self, section)
209 if not self.optional then
210 return
211 end
212
213 local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
214 for k,v in ipairs(self.children) do
215 if v.optional and not v:ucivalue(section) then
216 if field == v.option then
217 self.map:set(section, field, v.default)
218 field = nil
219 else
220 table.insert(self.optionals, v)
221 end
222 end
223 end
224
225 if field and field:len() > 0 and self.dynamic then
226 self:add_dynamic(field)
227 end
228 end
229
230 -- Add a dynamic option
231 function AbstractSection.add_dynamic(self, field, optional)
232 local o = self:option(Value, field, field)
233 o.optional = optional
234 end
235
236 -- Parse all dynamic options
237 function AbstractSection.parse_dynamic(self, section)
238 if not self.dynamic then
239 return
240 end
241
242 local arr = ffluci.util.clone(self:ucivalue(section))
243 local form = ffluci.http.formvalue("cbid."..self.config.."."..section)
244 if type(form) == "table" then
245 for k,v in pairs(form) do
246 arr[k] = v
247 end
248 end
249
250 for key,val in pairs(arr) do
251 local create = true
252
253 for i,c in ipairs(self.children) do
254 if c.option == key then
255 create = false
256 end
257 end
258
259 if create and key:sub(1, 1) ~= "." then
260 self:add_dynamic(key, true)
261 end
262 end
263 end
264
265 -- Returns the section's UCI table
266 function AbstractSection.ucivalue(self, section)
267 return self.map:get(section)
268 end
269
270
271
272 --[[
273 NamedSection - A fixed configuration section defined by its name
274 ]]--
275 NamedSection = class(AbstractSection)
276
277 function NamedSection.__init__(self, map, section, ...)
278 AbstractSection.__init__(self, map, ...)
279 self.template = "cbi/nsection"
280
281 self.section = section
282 self.addremove = false
283 end
284
285 function NamedSection.parse(self)
286 local active = self:ucivalue(self.section)
287
288 if self.addremove then
289 local path = self.config.."."..self.section
290 if active then -- Remove the section
291 if ffluci.http.formvalue("cbi.rns."..path) and self:remove() then
292 return
293 end
294 else -- Create and apply default values
295 if ffluci.http.formvalue("cbi.cns."..path) and self:create() then
296 for k,v in pairs(self.children) do
297 v:write(self.section, v.default)
298 end
299 end
300 end
301 end
302
303 if active then
304 AbstractSection.parse_dynamic(self, self.section)
305 Node.parse(self, self.section)
306 AbstractSection.parse_optionals(self, self.section)
307 end
308 end
309
310 -- Removes the section
311 function NamedSection.remove(self)
312 return self.map:del(self.section)
313 end
314
315 -- Creates the section
316 function NamedSection.create(self)
317 return self.map:set(self.section, nil, self.sectiontype)
318 end
319
320
321
322 --[[
323 TypedSection - A (set of) configuration section(s) defined by the type
324 addremove: Defines whether the user can add/remove sections of this type
325 anonymous: Allow creating anonymous sections
326 valid: a list of names or a validation function for creating sections
327 scope: a list of names or a validation function for editing sections
328 ]]--
329 TypedSection = class(AbstractSection)
330
331 function TypedSection.__init__(self, ...)
332 AbstractSection.__init__(self, ...)
333 self.template = "cbi/tsection"
334
335 self.anonymous = false
336 self.valid = nil
337 self.scope = nil
338 end
339
340 -- Creates a new section of this type with the given name (or anonymous)
341 function TypedSection.create(self, name)
342 if name then
343 self.map:set(name, nil, self.sectiontype)
344 else
345 name = self.map:add(self.sectiontype)
346 end
347
348 for k,v in pairs(self.children) do
349 if v.default then
350 self.map:set(name, v.option, v.default)
351 end
352 end
353 end
354
355 function TypedSection.parse(self)
356 if self.addremove then
357 -- Create
358 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
359 local name = ffluci.http.formvalue(crval)
360 if self.anonymous then
361 if name then
362 self:create()
363 end
364 else
365 if name then
366 name = ffluci.util.validate(name, self.valid)
367 if not name then
368 self.err_invalid = true
369 end
370 if name and name:len() > 0 then
371 self:create(name)
372 end
373 end
374 end
375
376 -- Remove
377 crval = "cbi.rts." .. self.config
378 name = ffluci.http.formvalue(crval)
379 if type(name) == "table" then
380 for k,v in pairs(name) do
381 if ffluci.util.validate(k, self.valid) then
382 self:remove(k)
383 end
384 end
385 end
386 end
387
388 for k, v in pairs(self:ucisections()) do
389 AbstractSection.parse_dynamic(self, k)
390 Node.parse(self, k)
391 AbstractSection.parse_optionals(self, k)
392 end
393 end
394
395 -- Remove a section
396 function TypedSection.remove(self, name)
397 return self.map:del(name)
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.ucisections(self)
409 local sections = {}
410 for k, v in pairs(self.map.ucidata) 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 = nil
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:ucivalue(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:ucivalue(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.ucivalue(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:ucivalue(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:ucivalue(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