* 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