7588a7fd6da28846d392bf20a7cb19a7f1bdb01b
[project/luci.git] / src / ffluci / cbi.lua
1 --[[
2 FFLuCI - Configuration Bind Interface
3
4 Description:
5 Offers an interface for binding confiugration values to certain
6 data types. Supports value and range validation and basic dependencies.
7
8 FileId:
9 $Id$
10
11 License:
12 Copyright 2008 Steven Barth <steven@midlink.org>
13
14 Licensed under the Apache License, Version 2.0 (the "License");
15 you may not use this file except in compliance with the License.
16 You may obtain a copy of the License at 
17
18         http://www.apache.org/licenses/LICENSE-2.0 
19
20 Unless required by applicable law or agreed to in writing, software
21 distributed under the License is distributed on an "AS IS" BASIS,
22 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 See the License for the specific language governing permissions and
24 limitations under the License.
25
26 ]]--
27 module("ffluci.cbi", package.seeall)
28
29 require("ffluci.template")
30 require("ffluci.util")
31 require("ffluci.http")
32 require("ffluci.model.uci")
33
34 local class      = ffluci.util.class
35 local instanceof = ffluci.util.instanceof
36
37 -- Loads a CBI map from given file, creating an environment and returns it
38 function load(cbimap)
39         require("ffluci.fs")
40         require("ffluci.i18n")
41         
42         local cbidir = ffluci.fs.dirname(ffluci.util.__file__()) .. "model/cbi/"
43         local func, err = loadfile(cbidir..cbimap..".lua")
44         
45         if not func then
46                 error(err)
47                 return nil
48         end
49         
50         ffluci.util.resfenv(func)
51         ffluci.util.updfenv(func, ffluci.cbi)
52         ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
53         
54         local map = func()
55         
56         if not instanceof(map, Map) then
57                 error("CBI map returns no valid map object!")
58                 return nil
59         end
60         
61         ffluci.i18n.loadc("cbi")
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                 self.ucidata = self.ucidata[self.config]
116         end     
117 end
118
119 -- Creates a child section
120 function Map.section(self, class, ...)
121         if instanceof(class, AbstractSection) then
122                 local obj  = class(self, ...)
123                 self:append(obj)
124                 return obj
125         else
126                 error("class must be a descendent of AbstractSection")
127         end
128 end
129
130 -- UCI add
131 function Map.add(self, sectiontype)
132         local name = self.uci:add(self.config, sectiontype)
133         if name then
134                 self.ucidata[name] = self.uci:show(self.config, name)
135         end
136         return name
137 end
138
139 -- UCI set
140 function Map.set(self, section, option, value)
141         local stat = self.uci:set(self.config, section, option, value)
142         if stat then
143                 local val = self.uci:get(self.config, section, option)
144                 if option then
145                         self.ucidata[section][option] = val
146                 else
147                         if not self.ucidata[section] then
148                                 self.ucidata[section] = {}
149                         end
150                         self.ucidata[section][".type"] = val
151                 end
152         end
153         return stat
154 end
155
156 -- UCI del
157 function Map.del(self, section, option)
158         local stat = self.uci:del(self.config, section, option)
159         if stat then
160                 if option then
161                         self.ucidata[section][option] = nil
162                 else
163                         self.ucidata[section] = nil
164                 end
165         end
166         return stat
167 end
168
169 -- UCI get (cached)
170 function Map.get(self, section, option)
171         if option and self.ucidata[section] then
172                 return self.ucidata[section][option]
173         else
174                 return self.ucidata[section]
175         end
176 end
177
178
179 --[[
180 AbstractSection
181 ]]--
182 AbstractSection = class(Node)
183
184 function AbstractSection.__init__(self, map, sectiontype, ...)
185         Node.__init__(self, ...)
186         self.sectiontype = sectiontype
187         self.map = map
188         self.config = map.config
189         self.optionals = {}
190         
191         self.addremove = true
192         self.optional = true
193         self.dynamic = false
194 end
195
196 -- Appends a new option
197 function AbstractSection.option(self, class, ...)
198         if instanceof(class, AbstractValue) then
199                 local obj  = class(self.map, ...)
200                 self:append(obj)
201                 return obj
202         else
203                 error("class must be a descendent of AbstractValue")
204         end     
205 end
206
207 -- Parse optional options
208 function AbstractSection.parse_optionals(self, section)
209         if not self.optional then
210                 return
211         end
212         
213         local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section)
214         for k,v in ipairs(self.children) do
215                 if v.optional and not v:ucivalue(section) then
216                         if field == v.option then
217                                 self.map:set(section, field, v.default)
218                                 field = nil
219                         else
220                                 table.insert(self.optionals, v)
221                         end
222                 end
223         end
224         
225         if field and field:len() > 0 and self.dynamic then
226                 self:add_dynamic(field)
227         end
228 end
229
230 -- Add a dynamic option
231 function AbstractSection.add_dynamic(self, field, optional)
232         local o = self:option(Value, field, field)
233         o.optional = optional
234 end
235
236 -- Parse all dynamic options
237 function AbstractSection.parse_dynamic(self, section)
238         if not self.dynamic then
239                 return
240         end
241         
242         local arr  = ffluci.util.clone(self:ucivalue(section))
243         local form = ffluci.http.formvalue("cbid."..self.config.."."..section)
244         if type(form) == "table" then
245                 for k,v in pairs(form) do
246                         arr[k] = v
247                 end
248         end     
249         
250         for key,val in pairs(arr) do
251                 local create = true
252                 
253                 for i,c in ipairs(self.children) do
254                         if c.option == key then
255                                 create = false
256                         end
257                 end
258                 
259                 if create and key:sub(1, 1) ~= "." then
260                         self:add_dynamic(key, true)
261                 end
262         end
263 end     
264
265 -- Returns the section's UCI table
266 function AbstractSection.ucivalue(self, section)
267         return self.map:get(section)
268 end
269
270
271
272 --[[
273 NamedSection - A fixed configuration section defined by its name
274 ]]--
275 NamedSection = class(AbstractSection)
276
277 function NamedSection.__init__(self, map, section, ...)
278         AbstractSection.__init__(self, map, ...)
279         self.template = "cbi/nsection"
280         
281         self.section = section
282         self.addremove = false
283 end
284
285 function NamedSection.parse(self)       
286         local active = self:ucivalue(self.section)
287         
288         if self.addremove then
289                 local path = self.config.."."..self.section
290                 if active then -- Remove the section
291                         if ffluci.http.formvalue("cbi.rns."..path) and self:remove() then
292                                 return
293                         end
294                 else           -- Create and apply default values
295                         if ffluci.http.formvalue("cbi.cns."..path) and self:create() then
296                                 for k,v in pairs(self.children) do
297                                         v:write(self.section, v.default)
298                                 end
299                         end
300                 end
301         end
302         
303         if active then
304                 AbstractSection.parse_dynamic(self, self.section)
305                 Node.parse(self, self.section)
306                 AbstractSection.parse_optionals(self, self.section)
307         end     
308 end
309
310 -- Removes the section
311 function NamedSection.remove(self)
312         return self.map:del(self.section)
313 end
314
315 -- Creates the section
316 function NamedSection.create(self)
317         return self.map:set(self.section, nil, self.sectiontype)
318 end
319
320
321
322 --[[
323 TypedSection - A (set of) configuration section(s) defined by the type
324         addremove:      Defines whether the user can add/remove sections of this type
325         anonymous:  Allow creating anonymous sections
326         valid:          a list of names or a validation function for creating sections 
327         scope:          a list of names or a validation function for editing sections
328 ]]--
329 TypedSection = class(AbstractSection)
330
331 function TypedSection.__init__(self, ...)
332         AbstractSection.__init__(self, ...)
333         self.template  = "cbi/tsection"
334         
335         self.anonymous   = false
336         self.valid       = nil
337         self.scope               = nil
338 end
339
340 -- Creates a new section of this type with the given name (or anonymous)
341 function TypedSection.create(self, name)
342         if name then    
343                 self.map:set(name, nil, self.sectiontype)
344         else
345                 name = self.map:add(self.sectiontype)
346         end
347         
348         for k,v in pairs(self.children) do
349                 if v.default then
350                         self.map:set(name, v.option, v.default)
351                 end
352         end
353 end
354
355 function TypedSection.parse(self)
356         if self.addremove then
357                 -- Create
358                 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
359                 local name  = ffluci.http.formvalue(crval)
360                 if self.anonymous then
361                         if name then
362                                 self:create()
363                         end
364                 else            
365                         if name then
366                                 name = ffluci.util.validate(name, self.valid)
367                                 if not name then
368                                         self.err_invalid = true
369                                 end             
370                                 if name and name:len() > 0 then
371                                         self:create(name)
372                                 end
373                         end
374                 end
375                 
376                 -- Remove
377                 crval = "cbi.rts." .. self.config
378                 name = ffluci.http.formvalue(crval)
379                 if type(name) == "table" then
380                         for k,v in pairs(name) do
381                                 if ffluci.util.validate(k, self.valid) then
382                                         self:remove(k)
383                                 end
384                         end
385                 end             
386         end
387         
388         for k, v in pairs(self:ucisections()) do
389                 AbstractSection.parse_dynamic(self, k)
390                 Node.parse(self, k)
391                 AbstractSection.parse_optionals(self, k)
392         end
393 end
394
395 -- Remove a section
396 function TypedSection.remove(self, name)
397         return self.map:del(name)
398 end
399
400 -- Render the children
401 function TypedSection.render_children(self, section)
402         for k, node in ipairs(self.children) do
403                 node:render(section)
404         end
405 end
406
407 -- Return all matching UCI sections for this TypedSection
408 function TypedSection.ucisections(self)
409         local sections = {}
410         for k, v in pairs(self.map.ucidata) do
411                 if v[".type"] == self.sectiontype then
412                         if ffluci.util.validate(k, self.scope) then
413                                 sections[k] = v
414                         end
415                 end
416         end
417         return sections 
418 end
419
420
421
422 --[[
423 AbstractValue - An abstract Value Type
424         null:           Value can be empty
425         valid:          A function returning the value if it is valid otherwise nil 
426         depends:        A table of option => value pairs of which one must be true
427         default:        The default value
428         size:           The size of the input fields
429         rmempty:        Unset value if empty
430         optional:       This value is optional (see AbstractSection.optionals)
431 ]]--
432 AbstractValue = class(Node)
433
434 function AbstractValue.__init__(self, map, option, ...)
435         Node.__init__(self, ...)
436         self.option = option
437         self.map    = map
438         self.config = map.config
439         self.tag_invalid = {}
440         
441         self.valid    = nil
442         self.depends  = nil
443         self.default  = nil
444         self.size     = nil
445         self.optional = false
446 end
447
448 -- Returns the formvalue for this object
449 function AbstractValue.formvalue(self, section)
450         local key = "cbid."..self.map.config.."."..section.."."..self.option
451         return ffluci.http.formvalue(key)
452 end
453
454 function AbstractValue.parse(self, section)
455         local fvalue = self:formvalue(section)
456         if fvalue == "" then
457                 fvalue = nil
458         end
459         
460         
461         if fvalue then -- If we have a form value, validate it and write it to UCI
462                 fvalue = self:validate(fvalue)
463                 if not fvalue then
464                         self.tag_invalid[section] = true
465                 end
466                 if fvalue and not (fvalue == self:ucivalue(section)) then
467                         self:write(section, fvalue)
468                 end 
469         elseif ffluci.http.formvalue("cbi.submit") then -- Unset the UCI or error
470                 if self.rmempty or self.optional then
471                         self:remove(section)
472                 else
473                         self.tag_invalid[section] = true
474                 end
475         end
476 end
477
478 -- Render if this value exists or if it is mandatory
479 function AbstractValue.render(self, section)
480         if not self.optional or self:ucivalue(section) then 
481                 ffluci.template.render(self.template, {self=self, section=section})
482         end
483 end
484
485 -- Return the UCI value of this object
486 function AbstractValue.ucivalue(self, section)
487         return self.map:get(section, self.option)
488 end
489
490 -- Validate the form value
491 function AbstractValue.validate(self, val)
492         return ffluci.util.validate(val, self.valid)
493 end
494
495 -- Write to UCI
496 function AbstractValue.write(self, section, value)
497         return self.map:set(section, self.option, value)
498 end
499
500 -- Remove from UCI
501 function AbstractValue.remove(self, section)
502         return self.map:del(section, self.option)
503 end
504
505
506
507
508 --[[
509 Value - A one-line value
510         maxlength:      The maximum length
511         isnumber:       The value must be a valid (floating point) number
512         isinteger:  The value must be a valid integer
513         ispositive: The value must be positive (and a number)
514 ]]--
515 Value = class(AbstractValue)
516
517 function Value.__init__(self, ...)
518         AbstractValue.__init__(self, ...)
519         self.template  = "cbi/value"
520         
521         self.maxlength  = nil
522         self.isnumber   = false
523         self.isinteger  = false
524 end
525
526 -- This validation is a bit more complex
527 function Value.validate(self, val)
528         if self.maxlength and tostring(val):len() > self.maxlength then
529                 val = nil
530         end
531         
532         return ffluci.util.validate(val, self.valid, self.isnumber, self.isinteger)
533 end
534
535
536
537 --[[
538 Flag - A flag being enabled or disabled
539 ]]--
540 Flag = class(AbstractValue)
541
542 function Flag.__init__(self, ...)
543         AbstractValue.__init__(self, ...)
544         self.template  = "cbi/fvalue"
545         
546         self.enabled = "1"
547         self.disabled = "0"
548 end
549
550 -- A flag can only have two states: set or unset
551 function Flag.parse(self, section)
552         self.default = self.enabled
553         local fvalue = self:formvalue(section)
554         
555         if fvalue then
556                 fvalue = self.enabled
557         else
558                 fvalue = self.disabled
559         end     
560         
561         if fvalue == self.enabled or (not self.optional and not self.rmempty) then              
562                 if not(fvalue == self:ucivalue(section)) then
563                         self:write(section, fvalue)
564                 end 
565         else
566                 self:remove(section)
567         end
568 end
569
570
571
572 --[[
573 ListValue - A one-line value predefined in a list
574         widget: The widget that will be used (select, radio)
575 ]]--
576 ListValue = class(AbstractValue)
577
578 function ListValue.__init__(self, ...)
579         AbstractValue.__init__(self, ...)
580         self.template  = "cbi/lvalue"
581         self.keylist = {}
582         self.vallist = {}
583         
584         self.size   = 1
585         self.widget = "select"
586 end
587
588 function ListValue.add_value(self, key, val)
589         val = val or key
590         table.insert(self.keylist, tostring(key))
591         table.insert(self.vallist, tostring(val)) 
592 end
593
594 function ListValue.validate(self, val)
595         if ffluci.util.contains(self.keylist, val) then
596                 return val
597         else
598                 return nil
599         end
600 end
601
602
603
604 --[[
605 MultiValue - Multiple delimited values
606         widget: The widget that will be used (select, checkbox)
607         delimiter: The delimiter that will separate the values (default: " ")
608 ]]--
609 MultiValue = class(AbstractValue)
610
611 function MultiValue.__init__(self, ...)
612         AbstractValue.__init__(self, ...)
613         self.template = "cbi/mvalue"
614         self.keylist = {}
615         self.vallist = {}       
616         
617         self.widget = "checkbox"
618         self.delimiter = " "
619 end
620
621 function MultiValue.add_value(self, key, val)
622         val = val or key
623         table.insert(self.keylist, tostring(key))
624         table.insert(self.vallist, tostring(val)) 
625 end
626
627 function MultiValue.valuelist(self, section)
628         local val = self:ucivalue(section)
629         
630         if not(type(val) == "string") then
631                 return {}
632         end
633         
634         return ffluci.util.split(val, self.delimiter)
635 end
636
637 function MultiValue.validate(self, val)
638         if not(type(val) == "string") then
639                 return nil
640         end
641         
642         local result = ""
643         
644         for value in val:gmatch("[^\n]+") do
645                 if ffluci.util.contains(self.keylist, value) then
646                         result = result .. self.delimiter .. value
647                 end 
648         end
649         
650         if result:len() > 0 then
651                 return result:sub(self.delimiter:len() + 1)
652         else
653                 return nil
654         end
655 end