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