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