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