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