ab342feec1e69e7eede7bedaffbf32b37ff8c60b
[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 FORM_NODATA  =  0
39 FORM_VALID   =  1
40 FORM_INVALID = -1
41
42 CREATE_PREFIX = "cbi.cts."
43 REMOVE_PREFIX = "cbi.rts."
44
45 -- Loads a CBI map from given file, creating an environment and returns it
46 function load(cbimap, ...)
47         require("luci.fs")
48         require("luci.i18n")
49         require("luci.config")
50         require("luci.util")
51
52         local cbidir = luci.util.libpath() .. "/model/cbi/"
53         local func, err = loadfile(cbidir..cbimap..".lua")
54
55         if not func then
56                 return nil
57         end
58
59         luci.i18n.loadc("cbi")
60
61         luci.util.resfenv(func)
62         luci.util.updfenv(func, luci.cbi)
63         luci.util.extfenv(func, "translate", luci.i18n.translate)
64         luci.util.extfenv(func, "translatef", luci.i18n.translatef)
65         luci.util.extfenv(func, "arg", {...})
66
67         local maps = {func()}
68
69         for i, map in ipairs(maps) do
70                 if not instanceof(map, Node) then
71                         error("CBI map returns no valid map object!")
72                         return nil
73                 end
74         end
75
76         return maps
77 end
78
79 -- Node pseudo abstract class
80 Node = class()
81
82 function Node.__init__(self, title, description)
83         self.children = {}
84         self.title = title or ""
85         self.description = description or ""
86         self.template = "cbi/node"
87 end
88
89 -- i18n helper
90 function Node._i18n(self, config, section, option, title, description)
91
92         -- i18n loaded?
93         if type(luci.i18n) == "table" then
94
95                 local key = config and config:gsub("[^%w]+", "") or ""
96
97                 if section then key = key .. "_" .. section:lower():gsub("[^%w]+", "") end
98                 if option  then key = key .. "_" .. tostring(option):lower():gsub("[^%w]+", "")  end
99
100                 self.title = title or luci.i18n.translate( key, option or section or config )
101                 self.description = description or luci.i18n.translate( key .. "_desc", "" )
102         end
103 end
104
105 -- Append child nodes
106 function Node.append(self, obj)
107         table.insert(self.children, obj)
108 end
109
110 -- Parse this node and its children
111 function Node.parse(self, ...)
112         for k, child in ipairs(self.children) do
113                 child:parse(...)
114         end
115 end
116
117 -- Render this node
118 function Node.render(self, scope)
119         scope = scope or {}
120         scope.self = self
121
122         luci.template.render(self.template, scope)
123 end
124
125 -- Render the children
126 function Node.render_children(self, ...)
127         for k, node in ipairs(self.children) do
128                 node:render(...)
129         end
130 end
131
132
133 --[[
134 A simple template element
135 ]]--
136 Template = class(Node)
137
138 function Template.__init__(self, template)
139         Node.__init__(self)
140         self.template = template
141 end
142
143 function Template.render(self)
144         luci.template.render(self.template, {self=self})
145 end
146
147
148 --[[
149 Map - A map describing a configuration file
150 ]]--
151 Map = class(Node)
152
153 function Map.__init__(self, config, ...)
154         Node.__init__(self, ...)
155         Node._i18n(self, config, nil, nil, ...)
156
157         self.config = config
158         self.parsechain = {self.config}
159         self.template = "cbi/map"
160         if not uci.load(self.config) then
161                 error("Unable to read UCI data: " .. self.config)
162         end
163 end
164
165
166 -- Chain foreign config
167 function Map.chain(self, config)
168         table.insert(self.parsechain, config)
169 end
170
171 -- Use optimized UCI writing
172 function Map.parse(self, ...)
173         Node.parse(self, ...)
174         for i, config in ipairs(self.parsechain) do
175                 uci.save(config)
176         end
177         if luci.http.formvalue("cbi.apply") then
178                 for i, config in ipairs(self.parsechain) do
179                         uci.commit(config)
180                         if luci.config.uci_oncommit and luci.config.uci_oncommit[config] then
181                                 luci.util.exec(luci.config.uci_oncommit[config])
182                         end
183
184                         -- Refresh data because commit changes section names
185                         uci.unload(config)
186                         uci.load(config)
187                 end
188
189                 -- Reparse sections
190                 Node.parse(self, ...)
191
192         end
193         for i, config in ipairs(self.parsechain) do
194                 uci.unload(config)
195         end
196 end
197
198 -- Creates a child section
199 function Map.section(self, class, ...)
200         if instanceof(class, AbstractSection) then
201                 local obj  = class(self, ...)
202                 self:append(obj)
203                 return obj
204         else
205                 error("class must be a descendent of AbstractSection")
206         end
207 end
208
209 -- UCI add
210 function Map.add(self, sectiontype)
211         return uci.add(self.config, sectiontype)
212 end
213
214 -- UCI set
215 function Map.set(self, section, option, value)
216         if option then
217                 return uci.set(self.config, section, option, value)
218         else
219                 return uci.set(self.config, section, value)
220         end
221 end
222
223 -- UCI del
224 function Map.del(self, section, option)
225         if option then
226                 return uci.delete(self.config, section, option)
227         else
228                 return uci.delete(self.config, section)
229         end
230 end
231
232 -- UCI get
233 function Map.get(self, section, option)
234         if not section then
235                 return uci.get_all(self.config)
236         elseif option then
237                 return uci.get(self.config, section, option)
238         else
239                 return uci.get_all(self.config, section)
240         end
241 end
242
243 -- UCI stateget
244 function Map.stateget(self, section, option)
245         return uci.get_statevalue(self.config, section, option)
246 end
247
248
249 --[[
250 Page - A simple node
251 ]]--
252
253 Page = class(Node)
254 Page.__init__ = Node.__init__
255 Page.parse    = function() end
256
257
258 --[[
259 SimpleForm - A Simple non-UCI form
260 ]]--
261 SimpleForm = class(Node)
262
263 function SimpleForm.__init__(self, config, title, description, data)
264         Node.__init__(self, title, description)
265         self.config = config
266         self.data = data or {}
267         self.template = "cbi/simpleform"
268         self.dorender = true
269 end
270
271 function SimpleForm.parse(self, ...)
272         if luci.http.formvalue("cbi.submit") then
273                 Node.parse(self, 1, ...)
274         end
275                 
276         local valid = true
277         for k, j in ipairs(self.children) do 
278                 for i, v in ipairs(j.children) do
279                         valid = valid 
280                          and (not v.tag_missing or not v.tag_missing[1])
281                          and (not v.tag_invalid or not v.tag_invalid[1])
282                 end
283         end
284         
285         local state = 
286                 not luci.http.formvalue("cbi.submit") and 0
287                 or valid and 1
288                 or -1
289
290         self.dorender = self:handle(state, self.data) ~= false
291 end
292
293 function SimpleForm.render(self, ...)
294         if self.dorender then
295                 Node.render(self, ...)
296         end
297 end
298
299 function SimpleForm.section(self, class, ...)
300         if instanceof(class, AbstractSection) then
301                 local obj  = class(self, ...)
302                 self:append(obj)
303                 return obj
304         else
305                 error("class must be a descendent of AbstractSection")
306         end
307 end
308
309 -- Creates a child field
310 function SimpleForm.field(self, class, ...)
311         local section
312         for k, v in ipairs(self.children) do
313                 if instanceof(v, SimpleSection) then
314                         section = v
315                         break
316                 end
317         end
318         if not section then
319                 section = self:section(SimpleSection)
320         end
321         
322         if instanceof(class, AbstractValue) then
323                 local obj  = class(self, ...)
324                 obj.track_missing = true
325                 section:append(obj)
326                 return obj
327         else
328                 error("class must be a descendent of AbstractValue")
329         end
330 end
331
332 function SimpleForm.set(self, section, option, value)
333         self.data[option] = value
334 end
335
336
337 function SimpleForm.del(self, section, option)
338         self.data[option] = nil
339 end
340
341
342 function SimpleForm.get(self, section, option)
343         return self.data[option]
344 end
345
346
347
348 --[[
349 AbstractSection
350 ]]--
351 AbstractSection = class(Node)
352
353 function AbstractSection.__init__(self, map, sectiontype, ...)
354         Node.__init__(self, ...)
355         self.sectiontype = sectiontype
356         self.map = map
357         self.config = map.config
358         self.optionals = {}
359         self.defaults = {}
360
361         self.optional = true
362         self.addremove = false
363         self.dynamic = false
364 end
365
366 -- Appends a new option
367 function AbstractSection.option(self, class, option, ...)
368         if instanceof(class, AbstractValue) then
369                 local obj  = class(self.map, option, ...)
370
371                 Node._i18n(obj, self.config, self.section or self.sectiontype, option, ...)
372
373                 self:append(obj)
374                 return obj
375         else
376                 error("class must be a descendent of AbstractValue")
377         end
378 end
379
380 -- Parse optional options
381 function AbstractSection.parse_optionals(self, section)
382         if not self.optional then
383                 return
384         end
385
386         self.optionals[section] = {}
387
388         local field = luci.http.formvalue("cbi.opt."..self.config.."."..section)
389         for k,v in ipairs(self.children) do
390                 if v.optional and not v:cfgvalue(section) then
391                         if field == v.option then
392                                 field = nil
393                         else
394                                 table.insert(self.optionals[section], v)
395                         end
396                 end
397         end
398
399         if field and #field > 0 and self.dynamic then
400                 self:add_dynamic(field)
401         end
402 end
403
404 -- Add a dynamic option
405 function AbstractSection.add_dynamic(self, field, optional)
406         local o = self:option(Value, field, field)
407         o.optional = optional
408 end
409
410 -- Parse all dynamic options
411 function AbstractSection.parse_dynamic(self, section)
412         if not self.dynamic then
413                 return
414         end
415
416         local arr  = luci.util.clone(self:cfgvalue(section))
417         local form = luci.http.formvaluetable("cbid."..self.config.."."..section)
418         for k, v in pairs(form) do
419                 arr[k] = v
420         end
421
422         for key,val in pairs(arr) do
423                 local create = true
424
425                 for i,c in ipairs(self.children) do
426                         if c.option == key then
427                                 create = false
428                         end
429                 end
430
431                 if create and key:sub(1, 1) ~= "." then
432                         self:add_dynamic(key, true)
433                 end
434         end
435 end
436
437 -- Returns the section's UCI table
438 function AbstractSection.cfgvalue(self, section)
439         return self.map:get(section)
440 end
441
442 -- Removes the section
443 function AbstractSection.remove(self, section)
444         return self.map:del(section)
445 end
446
447 -- Creates the section
448 function AbstractSection.create(self, section)
449         local stat
450         
451         if section then
452                 stat = self.map:set(section, nil, self.sectiontype)
453         else
454                 section = self.map:add(self.sectiontype)
455                 stat = section
456         end
457
458         if stat then
459                 for k,v in pairs(self.children) do
460                         if v.default then
461                                 self.map:set(section, v.option, v.default)
462                         end
463                 end
464
465                 for k,v in pairs(self.defaults) do
466                         self.map:set(section, k, v)
467                 end
468         end
469
470         return stat
471 end
472
473
474 SimpleSection = class(AbstractSection)
475
476 function SimpleSection.__init__(self, form, ...)
477         AbstractSection.__init__(self, form, nil, ...)
478         self.template = "cbi/nullsection"
479 end
480
481
482 Table = class(AbstractSection)
483
484 function Table.__init__(self, form, data, ...)
485         local datasource = {}
486         datasource.config = "table"
487         self.data = data
488         
489         function datasource.get(self, section, option)
490                 return data[section][option]
491         end
492         
493         AbstractSection.__init__(self, datasource, "table", ...)
494         self.template = "cbi/tblsection"
495 end
496
497 function Table.cfgsections(self)
498         local sections = {}
499         
500         for i, v in pairs(self.data) do
501                 table.insert(sections, i)
502         end
503         
504         return sections
505 end
506
507
508
509 --[[
510 NamedSection - A fixed configuration section defined by its name
511 ]]--
512 NamedSection = class(AbstractSection)
513
514 function NamedSection.__init__(self, map, section, type, ...)
515         AbstractSection.__init__(self, map, type, ...)
516         Node._i18n(self, map.config, section, nil, ...)
517
518         self.template = "cbi/nsection"
519         self.section = section
520         self.addremove = false
521 end
522
523 function NamedSection.parse(self)
524         local s = self.section
525         local active = self:cfgvalue(s)
526
527
528         if self.addremove then
529                 local path = self.config.."."..s
530                 if active then -- Remove the section
531                         if luci.http.formvalue("cbi.rns."..path) and self:remove(s) then
532                                 return
533                         end
534                 else           -- Create and apply default values
535                         if luci.http.formvalue("cbi.cns."..path) then
536                                 self:create(s)
537                                 return
538                         end
539                 end
540         end
541
542         if active then
543                 AbstractSection.parse_dynamic(self, s)
544                 if luci.http.formvalue("cbi.submit") then
545                         Node.parse(self, s)
546                 end
547                 AbstractSection.parse_optionals(self, s)
548         end
549 end
550
551
552 --[[
553 TypedSection - A (set of) configuration section(s) defined by the type
554         addremove:      Defines whether the user can add/remove sections of this type
555         anonymous:  Allow creating anonymous sections
556         validate:       a validation function returning nil if the section is invalid
557 ]]--
558 TypedSection = class(AbstractSection)
559
560 function TypedSection.__init__(self, map, type, ...)
561         AbstractSection.__init__(self, map, type, ...)
562         Node._i18n(self, map.config, type, nil, ...)
563
564         self.template  = "cbi/tsection"
565         self.deps = {}
566
567         self.anonymous = false
568 end
569
570 -- Return all matching UCI sections for this TypedSection
571 function TypedSection.cfgsections(self)
572         local sections = {}
573         uci.foreach(self.map.config, self.sectiontype,
574                 function (section)
575                         if self:checkscope(section[".name"]) then
576                                 table.insert(sections, section[".name"])
577                         end
578                 end)
579
580         return sections
581 end
582
583 -- Limits scope to sections that have certain option => value pairs
584 function TypedSection.depends(self, option, value)
585         table.insert(self.deps, {option=option, value=value})
586 end
587
588 function TypedSection.parse(self)
589         if self.addremove then
590                 -- Create
591                 local crval = CREATE_PREFIX .. self.config .. "." .. self.sectiontype
592                 local name  = luci.http.formvalue(crval)
593                 if self.anonymous then
594                         if name then
595                                 self:create()
596                         end
597                 else
598                         if name then
599                                 -- Ignore if it already exists
600                                 if self:cfgvalue(name) then
601                                         name = nil;
602                                 end
603
604                                 name = self:checkscope(name)
605
606                                 if not name then
607                                         self.err_invalid = true
608                                 end
609
610                                 if name and name:len() > 0 then
611                                         self:create(name)
612                                 end
613                         end
614                 end
615
616                 -- Remove
617                 crval = REMOVE_PREFIX .. self.config
618                 name = luci.http.formvaluetable(crval)
619                 for k,v in pairs(name) do
620                         if self:cfgvalue(k) and self:checkscope(k) then
621                                 self:remove(k)
622                         end
623                 end
624         end
625
626         for i, k in ipairs(self:cfgsections()) do
627                 AbstractSection.parse_dynamic(self, k)
628                 if luci.http.formvalue("cbi.submit") then
629                         Node.parse(self, k)
630                 end
631                 AbstractSection.parse_optionals(self, k)
632         end
633 end
634
635 -- Verifies scope of sections
636 function TypedSection.checkscope(self, section)
637         -- Check if we are not excluded
638         if self.filter and not self:filter(section) then
639                 return nil
640         end
641
642         -- Check if at least one dependency is met
643         if #self.deps > 0 and self:cfgvalue(section) then
644                 local stat = false
645
646                 for k, v in ipairs(self.deps) do
647                         if self:cfgvalue(section)[v.option] == v.value then
648                                 stat = true
649                         end
650                 end
651
652                 if not stat then
653                         return nil
654                 end
655         end
656
657         return self:validate(section)
658 end
659
660
661 -- Dummy validate function
662 function TypedSection.validate(self, section)
663         return section
664 end
665
666
667 --[[
668 AbstractValue - An abstract Value Type
669         null:           Value can be empty
670         valid:          A function returning the value if it is valid otherwise nil
671         depends:        A table of option => value pairs of which one must be true
672         default:        The default value
673         size:           The size of the input fields
674         rmempty:        Unset value if empty
675         optional:       This value is optional (see AbstractSection.optionals)
676 ]]--
677 AbstractValue = class(Node)
678
679 function AbstractValue.__init__(self, map, option, ...)
680         Node.__init__(self, ...)
681         self.option = option
682         self.map    = map
683         self.config = map.config
684         self.tag_invalid = {}
685         self.tag_missing = {}
686         self.deps = {}
687
688         self.track_missing = false
689         self.rmempty   = false
690         self.default   = nil
691         self.size      = nil
692         self.optional  = false
693         self.stateful  = false
694 end
695
696 -- Add a dependencie to another section field
697 function AbstractValue.depends(self, field, value)
698         table.insert(self.deps, {field=field, value=value})
699 end
700
701 -- Return whether this object should be created
702 function AbstractValue.formcreated(self, section)
703         local key = "cbi.opt."..self.config.."."..section
704         return (luci.http.formvalue(key) == self.option)
705 end
706
707 -- Returns the formvalue for this object
708 function AbstractValue.formvalue(self, section)
709         local key = "cbid."..self.map.config.."."..section.."."..self.option
710         return luci.http.formvalue(key)
711 end
712
713 function AbstractValue.additional(self, value)
714         self.optional = value
715 end
716
717 function AbstractValue.mandatory(self, value)
718         self.rmempty = not value
719 end
720
721 function AbstractValue.parse(self, section)
722         local fvalue = self:formvalue(section)
723         local cvalue = self:cfgvalue(section)
724
725         if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI
726                 fvalue = self:transform(self:validate(fvalue, section))
727                 if not fvalue then
728                         self.tag_invalid[section] = true
729                 end
730                 if fvalue and not (fvalue == cvalue) then
731                         self:write(section, fvalue)
732                 end
733         else                                                    -- Unset the UCI or error
734                 if self.rmempty or self.optional then
735                         self:remove(section)
736                 elseif self.track_missing and (not fvalue or fvalue ~= cvalue) then
737                         self.tag_missing[section] = true
738                 end
739         end
740 end
741
742 -- Render if this value exists or if it is mandatory
743 function AbstractValue.render(self, s, scope)
744         if not self.optional or self:cfgvalue(s) or self:formcreated(s) then
745                 scope = scope or {}
746                 scope.section = s
747                 scope.cbid    = "cbid." .. self.config ..
748                                 "."     .. s           ..
749                                                 "."     .. self.option
750
751                 scope.ifattr = function(cond,key,val)
752                         if cond then
753                                 return string.format(
754                                         ' %s="%s"', tostring(key),
755                                         luci.util.pcdata(tostring( val
756                                          or scope[key]
757                                          or (type(self[key]) ~= "function" and self[key])
758                                          or "" ))
759                                 )
760                         else
761                                 return ''
762                         end
763                 end
764
765                 scope.attr = function(...)
766                         return scope.ifattr( true, ... )
767                 end
768
769                 Node.render(self, scope)
770         end
771 end
772
773 -- Return the UCI value of this object
774 function AbstractValue.cfgvalue(self, section)
775         return self.stateful
776          and self.map:stateget(section, self.option)
777          or  self.map:get(section, self.option)
778 end
779
780 -- Validate the form value
781 function AbstractValue.validate(self, value)
782         return value
783 end
784
785 AbstractValue.transform = AbstractValue.validate
786
787
788 -- Write to UCI
789 function AbstractValue.write(self, section, value)
790         return self.map:set(section, self.option, value)
791 end
792
793 -- Remove from UCI
794 function AbstractValue.remove(self, section)
795         return self.map:del(section, self.option)
796 end
797
798
799
800
801 --[[
802 Value - A one-line value
803         maxlength:      The maximum length
804 ]]--
805 Value = class(AbstractValue)
806
807 function Value.__init__(self, ...)
808         AbstractValue.__init__(self, ...)
809         self.template  = "cbi/value"
810         self.keylist = {}
811         self.vallist = {}
812 end
813
814 function Value.value(self, key, val)
815         val = val or key
816         table.insert(self.keylist, tostring(key))
817         table.insert(self.vallist, tostring(val))
818 end
819
820
821 -- DummyValue - This does nothing except being there
822 DummyValue = class(AbstractValue)
823
824 function DummyValue.__init__(self, map, ...)
825         AbstractValue.__init__(self, map, ...)
826         self.template = "cbi/dvalue"
827         self.value = nil
828 end
829
830 function DummyValue.parse(self)
831
832 end
833
834
835 --[[
836 Flag - A flag being enabled or disabled
837 ]]--
838 Flag = class(AbstractValue)
839
840 function Flag.__init__(self, ...)
841         AbstractValue.__init__(self, ...)
842         self.template  = "cbi/fvalue"
843
844         self.enabled = "1"
845         self.disabled = "0"
846 end
847
848 -- A flag can only have two states: set or unset
849 function Flag.parse(self, section)
850         local fvalue = self:formvalue(section)
851
852         if fvalue then
853                 fvalue = self.enabled
854         else
855                 fvalue = self.disabled
856         end
857
858         if fvalue == self.enabled or (not self.optional and not self.rmempty) then
859                 if not(fvalue == self:cfgvalue(section)) then
860                         self:write(section, fvalue)
861                 end
862         else
863                 self:remove(section)
864         end
865 end
866
867
868
869 --[[
870 ListValue - A one-line value predefined in a list
871         widget: The widget that will be used (select, radio)
872 ]]--
873 ListValue = class(AbstractValue)
874
875 function ListValue.__init__(self, ...)
876         AbstractValue.__init__(self, ...)
877         self.template  = "cbi/lvalue"
878         self.keylist = {}
879         self.vallist = {}
880
881         self.size   = 1
882         self.widget = "select"
883 end
884
885 function ListValue.value(self, key, val)
886         val = val or key
887         table.insert(self.keylist, tostring(key))
888         table.insert(self.vallist, tostring(val))
889 end
890
891 function ListValue.validate(self, val)
892         if luci.util.contains(self.keylist, val) then
893                 return val
894         else
895                 return nil
896         end
897 end
898
899
900
901 --[[
902 MultiValue - Multiple delimited values
903         widget: The widget that will be used (select, checkbox)
904         delimiter: The delimiter that will separate the values (default: " ")
905 ]]--
906 MultiValue = class(AbstractValue)
907
908 function MultiValue.__init__(self, ...)
909         AbstractValue.__init__(self, ...)
910         self.template = "cbi/mvalue"
911         self.keylist = {}
912         self.vallist = {}
913
914         self.widget = "checkbox"
915         self.delimiter = " "
916 end
917
918 function MultiValue.render(self, ...)
919         if self.widget == "select" and not self.size then
920                 self.size = #self.vallist
921         end
922
923         AbstractValue.render(self, ...)
924 end
925
926 function MultiValue.value(self, key, val)
927         val = val or key
928         table.insert(self.keylist, tostring(key))
929         table.insert(self.vallist, tostring(val))
930 end
931
932 function MultiValue.valuelist(self, section)
933         local val = self:cfgvalue(section)
934
935         if not(type(val) == "string") then
936                 return {}
937         end
938
939         return luci.util.split(val, self.delimiter)
940 end
941
942 function MultiValue.validate(self, val)
943         val = (type(val) == "table") and val or {val}
944
945         local result
946
947         for i, value in ipairs(val) do
948                 if luci.util.contains(self.keylist, value) then
949                         result = result and (result .. self.delimiter .. value) or value
950                 end
951         end
952
953         return result
954 end
955
956 --[[
957 TextValue - A multi-line value
958         rows:   Rows
959 ]]--
960 TextValue = class(AbstractValue)
961
962 function TextValue.__init__(self, ...)
963         AbstractValue.__init__(self, ...)
964         self.template  = "cbi/tvalue"
965 end