ac5c1dd84f4de574f44157774e4cf06e5a82cbc1
[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 Template   = ffluci.template.Template
35 local class      = ffluci.util.class
36 local instanceof = ffluci.util.instanceof
37
38
39 function load(cbimap)
40         require("ffluci.fs")
41         require("ffluci.i18n")
42         
43         local cbidir = ffluci.fs.dirname(ffluci.util.__file__()) .. "model/cbi/"
44         local func, err = loadfile(cbidir..cbimap..".lua")
45         
46         if not func then
47                 error(err)
48                 return nil
49         end
50         
51         ffluci.util.resfenv(func)
52         ffluci.util.updfenv(func, ffluci.cbi)
53         ffluci.util.extfenv(func, "translate", ffluci.i18n.translate)
54         
55         local map = func()
56         
57         if not instanceof(map, Map) then
58                 error("CBI map returns no valid map object!")
59                 return nil
60         end
61         
62         ffluci.i18n.loadc("cbi")
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 function Node.append(self, obj)
78         table.insert(self.children, obj)
79 end
80
81 function Node.parse(self)
82         for k, child in ipairs(self.children) do
83                 child:parse()
84         end
85 end
86
87 function Node.render(self)
88         ffluci.template.render(self.template, {self=self})
89 end
90
91 function Node.render_children(self)
92         for k, node in ipairs(self.children) do
93                 node:render()
94         end
95 end
96
97
98 --[[
99 Map - A map describing a configuration file 
100 ]]--
101 Map = class(Node)
102
103 function Map.__init__(self, config, ...)
104         Node.__init__(self, ...)
105         self.config = config
106         self.template = "cbi/map"
107         self.uci = ffluci.model.uci.Session()
108 end
109
110 function Map.parse(self)
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         Node.parse(self)
118 end
119
120 function Map.render(self)
121         self.ucidata = self.uci:show(self.config)
122         if not self.ucidata then
123                 error("Unable to read UCI data: " .. self.config)
124         else
125                 self.ucidata = self.ucidata[self.config]
126         end
127         Node.render(self)       
128 end
129
130 function Map.section(self, class, ...)
131         if instanceof(class, AbstractSection) then
132                 local obj  = class(...)
133                 obj.map    = self
134                 obj.config = self.config
135                 self:append(obj)
136                 return obj
137         else
138                 error("class must be a descendent of AbstractSection")
139         end
140 end
141
142 function Map.add(self, sectiontype)
143         return self.uci:add(self.config, sectiontype)
144 end
145
146 function Map.set(self, section, option, value)
147         return self.uci:set(self.config, section, option, value)
148 end
149
150 function Map.remove(self, section, option)
151         return self.uci:del(self.config, section, option)
152 end
153
154 --[[
155 AbstractSection
156 ]]--
157 AbstractSection = class(Node)
158
159 function AbstractSection.__init__(self, sectiontype, ...)
160         Node.__init__(self, ...)
161         self.sectiontype = sectiontype
162 end
163
164 function AbstractSection.option(self, class, ...)
165         if instanceof(class, AbstractValue) then
166                 local obj  = class(...)
167                 obj.map    = self.map
168                 obj.config = self.config
169                 self:append(obj)
170                 return obj
171         else
172                 error("class must be a descendent of AbstractValue")
173         end     
174 end
175         
176
177
178 --[[
179 NamedSection - A fixed configuration section defined by its name
180 ]]--
181 NamedSection = class(AbstractSection)
182
183 function NamedSection.__init__(self, section, ...)
184         AbstractSection.__init__(self, ...)
185         self.template = "cbi/nsection"
186         
187         self.section = section
188 end
189
190 function NamedSection.option(self, ...)
191         local obj = AbstractSection.option(self, ...)
192         obj.section = self.section
193         return obj
194 end
195
196
197 --[[
198 TypedSection - A (set of) configuration section(s) defined by the type
199         addremove:      Defines whether the user can add/remove sections of this type
200         anonymous:  Allow creating anonymous sections
201         valid:          a list of names or a validation function for creating sections 
202         scope:          a list of names or a validation function for editing sections
203 ]]--
204 TypedSection = class(AbstractSection)
205
206 function TypedSection.__init__(self, ...)
207         AbstractSection.__init__(self, ...)
208         self.template  = "cbi/tsection"
209         
210         self.addremove   = true
211         self.anonymous   = false
212         self.valid       = nil
213         self.scope               = nil
214 end
215
216 function TypedSection.create(self, name)
217         if name then    
218                 self.map:set(name, nil, self.sectiontype)
219         else
220                 name = self.map:add(self.sectiontype)
221         end
222         
223         for k,v in pairs(self.children) do
224                 if v.default then
225                         self.map:set(name, v.option, v.default)
226                 end
227         end
228 end
229
230 function TypedSection.parse(self)
231         if self.addremove then
232                 local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype
233                 local name  = ffluci.http.formvalue(crval)
234                 if self.anonymous then
235                         if name then
236                                 self:create()
237                         end
238                 else            
239                         if name then
240                                 name = ffluci.util.validate(name, self.valid)
241                                 if not name then
242                                         self.err_invalid = true
243                                 end             
244                                 if name and name:len() > 0 then
245                                         self:create(name)
246                                 end
247                         end
248                 end
249                 
250                 
251                 crval = "cbi.rts." .. self.config
252                 name = ffluci.http.formvalue(crval)
253                 if type(name) == "table" then
254                         for k,v in pairs(name) do
255                                 if ffluci.util.validate(k, self.valid) then
256                                         self:remove(k)
257                                 end
258                         end
259                 end             
260         end
261         
262         for k, v in pairs(self:ucisections()) do
263                 for i, node in ipairs(self.children) do
264                         node.section = k
265                         node:parse()
266                 end 
267         end
268 end
269
270 function TypedSection.remove(self, name)
271         return self.map:remove(name)
272 end
273
274 function TypedSection.render_children(self, section)
275         for k, node in ipairs(self.children) do
276                 node.section = section
277                 node:render()
278         end
279 end
280
281 function TypedSection.ucisections(self)
282         local sections = {}
283         for k, v in pairs(self.map.ucidata) do
284                 if v[".type"] == self.sectiontype then
285                         if ffluci.util.validate(k, self.scope) then
286                                 sections[k] = v
287                         end
288                 end
289         end
290         return sections 
291 end
292
293
294 --[[
295 AbstractValue - An abstract Value Type
296         null:           Value can be empty
297         valid:          A function returning the value if it is valid otherwise nil 
298         depends:        A table of option => value pairs of which one must be true
299         default:        The default value
300         size:           The size of the input fields
301 ]]--
302 AbstractValue = class(Node)
303
304 function AbstractValue.__init__(self, option, ...)
305         Node.__init__(self, ...)
306         self.option  = option
307         
308         self.valid   = nil
309         self.depends = nil
310         self.default = nil
311         self.size    = nil
312 end
313
314 function AbstractValue.formvalue(self)
315         local key = "cbid."..self.map.config.."."..self.section.."."..self.option
316         return ffluci.http.formvalue(key)
317 end
318
319 function AbstractValue.parse(self)
320         local fvalue = self:formvalue()
321         if fvalue then
322                 fvalue = self:validate(fvalue)
323                 if not fvalue then
324                         self.err_invalid = true
325                 end
326                 if fvalue and not (fvalue == self:ucivalue()) then
327                         self:write(fvalue)
328                 end 
329         end
330 end
331
332 function AbstractValue.ucivalue(self)
333         return self.map.ucidata[self.section][self.option]
334 end
335
336 function AbstractValue.validate(self, val)
337         return ffluci.util.validate(val, self.valid)
338 end
339
340 function AbstractValue.write(self, value)
341         return self.map:set(self.section, self.option, value)
342 end
343
344
345
346
347 --[[
348 Value - A one-line value
349         maxlength:      The maximum length
350         isnumber:       The value must be a valid (floating point) number
351         isinteger:  The value must be a valid integer
352         ispositive: The value must be positive (and a number)
353 ]]--
354 Value = class(AbstractValue)
355
356 function Value.__init__(self, ...)
357         AbstractValue.__init__(self, ...)
358         self.template  = "cbi/value"
359         
360         self.maxlength  = nil
361         self.isnumber   = false
362         self.isinteger  = false
363 end
364
365 function Value.validate(self, val)
366         if self.maxlength and tostring(val):len() > self.maxlength then
367                 val = nil
368         end
369         
370         return ffluci.util.validate(val, self.valid, self.isnumber, self.isinteger)
371 end
372
373
374 --[[
375 ListValue - A one-line value predefined in a list
376 ]]--
377 ListValue = class(AbstractValue)
378
379 function ListValue.__init__(self, ...)
380         AbstractValue.__init__(self, ...)
381         self.template  = "cbi/lvalue"
382         
383         self.list = {}
384 end
385
386 function ListValue.add_value(self, key, val)
387         val = val or key
388         self.list[key] = val
389 end