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