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