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