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