* CBI: updates
[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