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