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