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