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