From 5f9910566de7165f4bb0ee62bc3ace53c708a94e Mon Sep 17 00:00:00 2001 From: Steven Barth Date: Tue, 25 Mar 2008 23:34:21 +0000 Subject: [PATCH] * Entering Version 0.2 * Completed CBI * Minor bugfixes and enhancements --- contrib/media/cascade.css | 39 +++- contrib/package/ffluci/Makefile | 4 +- src/ffluci/cbi.lua | 400 ++++++++++++++++++++++++++++++++------- src/ffluci/init.lua | 2 +- src/ffluci/util.lua | 39 ++++ src/ffluci/view/cbi/fvalue.htm | 10 + src/ffluci/view/cbi/header.htm | 1 + src/ffluci/view/cbi/lvalue.htm | 26 ++- src/ffluci/view/cbi/mvalue.htm | 26 +++ src/ffluci/view/cbi/nsection.htm | 37 +++- src/ffluci/view/cbi/tsection.htm | 28 ++- src/ffluci/view/cbi/value.htm | 11 +- 12 files changed, 525 insertions(+), 98 deletions(-) create mode 100644 src/ffluci/view/cbi/fvalue.htm create mode 100644 src/ffluci/view/cbi/mvalue.htm diff --git a/contrib/media/cascade.css b/contrib/media/cascade.css index 7717b184a..8b192ba7c 100644 --- a/contrib/media/cascade.css +++ b/contrib/media/cascade.css @@ -157,17 +157,26 @@ h2 { } -.cbi-value-title, .cbi-lvalue-title { - float: left; +.cbi-section { + margin-top: 1em; +} + +.cbi-section-create { + margin-bottom: 3em; +} + +.cbi-value-title { font-weight: bold; - line-height: 2em; + line-height: 1.75em; } -.cbi-value-field, .cbi-lvalue-field { +.cbi-value-field { text-align: right; + vertical-align: center; + line-height: 1.75em; } -.cbi-value-description, .cbi-lvalue-description { +.cbi-value-description { clear: both; font-style: italic; font-size: 0.8em; @@ -181,8 +190,26 @@ h2 { margin-top: 1em; } -.cbi-tsection-node { +.cbi-section-node { margin-top: 1em; border: none; background-color: #eeeeee; +} + +.cbi-error { + color: red; + font-weight: bold; +} + +.cbi-optionals { + margin-top: 2em; +} + +.cbi-optionals select { + height: 1.5em; + width: 20em; +} + +.cbi-optionals option { + font-size: 0.8em; } \ No newline at end of file diff --git a/contrib/package/ffluci/Makefile b/contrib/package/ffluci/Makefile index dab80be37..9803d3e6b 100644 --- a/contrib/package/ffluci/Makefile +++ b/contrib/package/ffluci/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ffluci -PKG_VERSION:=0.1 +PKG_VERSION:=0.2 PKG_RELEASE:=1 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION) @@ -28,7 +28,7 @@ define Build/Configure endef define Build/Compile - $(MAKE) -C $(PKG_BUILD_DIR) dist-source + $(MAKE) -C $(PKG_BUILD_DIR) $(MAKE_ACTION) endef define Package/ffluci/install diff --git a/src/ffluci/cbi.lua b/src/ffluci/cbi.lua index ac5c1dd84..7588a7fd6 100644 --- a/src/ffluci/cbi.lua +++ b/src/ffluci/cbi.lua @@ -31,11 +31,10 @@ require("ffluci.util") require("ffluci.http") require("ffluci.model.uci") -local Template = ffluci.template.Template local class = ffluci.util.class local instanceof = ffluci.util.instanceof - +-- Loads a CBI map from given file, creating an environment and returns it function load(cbimap) require("ffluci.fs") require("ffluci.i18n") @@ -74,23 +73,27 @@ function Node.__init__(self, title, description) self.template = "cbi/node" end +-- Append child nodes function Node.append(self, obj) table.insert(self.children, obj) end -function Node.parse(self) +-- Parse this node and its children +function Node.parse(self, ...) for k, child in ipairs(self.children) do - child:parse() + child:parse(...) end end +-- Render this node function Node.render(self) ffluci.template.render(self.template, {self=self}) end -function Node.render_children(self) +-- Render the children +function Node.render_children(self, ...) for k, node in ipairs(self.children) do - node:render() + node:render(...) end end @@ -105,33 +108,18 @@ function Map.__init__(self, config, ...) self.config = config self.template = "cbi/map" self.uci = ffluci.model.uci.Session() -end - -function Map.parse(self) self.ucidata = self.uci:show(self.config) if not self.ucidata then error("Unable to read UCI data: " .. self.config) else self.ucidata = self.ucidata[self.config] - end - Node.parse(self) -end - -function Map.render(self) - self.ucidata = self.uci:show(self.config) - if not self.ucidata then - error("Unable to read UCI data: " .. self.config) - else - self.ucidata = self.ucidata[self.config] - end - Node.render(self) + end end +-- Creates a child section function Map.section(self, class, ...) if instanceof(class, AbstractSection) then - local obj = class(...) - obj.map = self - obj.config = self.config + local obj = class(self, ...) self:append(obj) return obj else @@ -139,40 +127,146 @@ function Map.section(self, class, ...) end end +-- UCI add function Map.add(self, sectiontype) - return self.uci:add(self.config, sectiontype) + local name = self.uci:add(self.config, sectiontype) + if name then + self.ucidata[name] = self.uci:show(self.config, name) + end + return name end +-- UCI set function Map.set(self, section, option, value) - return self.uci:set(self.config, section, option, value) + local stat = self.uci:set(self.config, section, option, value) + if stat then + local val = self.uci:get(self.config, section, option) + if option then + self.ucidata[section][option] = val + else + if not self.ucidata[section] then + self.ucidata[section] = {} + end + self.ucidata[section][".type"] = val + end + end + return stat end -function Map.remove(self, section, option) - return self.uci:del(self.config, section, option) +-- UCI del +function Map.del(self, section, option) + local stat = self.uci:del(self.config, section, option) + if stat then + if option then + self.ucidata[section][option] = nil + else + self.ucidata[section] = nil + end + end + return stat +end + +-- UCI get (cached) +function Map.get(self, section, option) + if option and self.ucidata[section] then + return self.ucidata[section][option] + else + return self.ucidata[section] + end end + --[[ AbstractSection ]]-- AbstractSection = class(Node) -function AbstractSection.__init__(self, sectiontype, ...) +function AbstractSection.__init__(self, map, sectiontype, ...) Node.__init__(self, ...) self.sectiontype = sectiontype + self.map = map + self.config = map.config + self.optionals = {} + + self.addremove = true + self.optional = true + self.dynamic = false end +-- Appends a new option function AbstractSection.option(self, class, ...) if instanceof(class, AbstractValue) then - local obj = class(...) - obj.map = self.map - obj.config = self.config + local obj = class(self.map, ...) self:append(obj) return obj else error("class must be a descendent of AbstractValue") end end + +-- Parse optional options +function AbstractSection.parse_optionals(self, section) + if not self.optional then + return + end + + local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section) + for k,v in ipairs(self.children) do + if v.optional and not v:ucivalue(section) then + if field == v.option then + self.map:set(section, field, v.default) + field = nil + else + table.insert(self.optionals, v) + end + end + end + + if field and field:len() > 0 and self.dynamic then + self:add_dynamic(field) + end +end + +-- Add a dynamic option +function AbstractSection.add_dynamic(self, field, optional) + local o = self:option(Value, field, field) + o.optional = optional +end + +-- Parse all dynamic options +function AbstractSection.parse_dynamic(self, section) + if not self.dynamic then + return + end + local arr = ffluci.util.clone(self:ucivalue(section)) + local form = ffluci.http.formvalue("cbid."..self.config.."."..section) + if type(form) == "table" then + for k,v in pairs(form) do + arr[k] = v + end + end + + for key,val in pairs(arr) do + local create = true + + for i,c in ipairs(self.children) do + if c.option == key then + create = false + end + end + + if create and key:sub(1, 1) ~= "." then + self:add_dynamic(key, true) + end + end +end + +-- Returns the section's UCI table +function AbstractSection.ucivalue(self, section) + return self.map:get(section) +end + --[[ @@ -180,20 +274,51 @@ NamedSection - A fixed configuration section defined by its name ]]-- NamedSection = class(AbstractSection) -function NamedSection.__init__(self, section, ...) - AbstractSection.__init__(self, ...) +function NamedSection.__init__(self, map, section, ...) + AbstractSection.__init__(self, map, ...) self.template = "cbi/nsection" self.section = section + self.addremove = false +end + +function NamedSection.parse(self) + local active = self:ucivalue(self.section) + + if self.addremove then + local path = self.config.."."..self.section + if active then -- Remove the section + if ffluci.http.formvalue("cbi.rns."..path) and self:remove() then + return + end + else -- Create and apply default values + if ffluci.http.formvalue("cbi.cns."..path) and self:create() then + for k,v in pairs(self.children) do + v:write(self.section, v.default) + end + end + end + end + + if active then + AbstractSection.parse_dynamic(self, self.section) + Node.parse(self, self.section) + AbstractSection.parse_optionals(self, self.section) + end end -function NamedSection.option(self, ...) - local obj = AbstractSection.option(self, ...) - obj.section = self.section - return obj +-- Removes the section +function NamedSection.remove(self) + return self.map:del(self.section) +end + +-- Creates the section +function NamedSection.create(self) + return self.map:set(self.section, nil, self.sectiontype) end + --[[ TypedSection - A (set of) configuration section(s) defined by the type addremove: Defines whether the user can add/remove sections of this type @@ -207,12 +332,12 @@ function TypedSection.__init__(self, ...) AbstractSection.__init__(self, ...) self.template = "cbi/tsection" - self.addremove = true self.anonymous = false self.valid = nil self.scope = nil end +-- Creates a new section of this type with the given name (or anonymous) function TypedSection.create(self, name) if name then self.map:set(name, nil, self.sectiontype) @@ -229,6 +354,7 @@ end function TypedSection.parse(self) if self.addremove then + -- Create local crval = "cbi.cts." .. self.config .. "." .. self.sectiontype local name = ffluci.http.formvalue(crval) if self.anonymous then @@ -247,7 +373,7 @@ function TypedSection.parse(self) end end - + -- Remove crval = "cbi.rts." .. self.config name = ffluci.http.formvalue(crval) if type(name) == "table" then @@ -260,24 +386,25 @@ function TypedSection.parse(self) end for k, v in pairs(self:ucisections()) do - for i, node in ipairs(self.children) do - node.section = k - node:parse() - end + AbstractSection.parse_dynamic(self, k) + Node.parse(self, k) + AbstractSection.parse_optionals(self, k) end end +-- Remove a section function TypedSection.remove(self, name) - return self.map:remove(name) + return self.map:del(name) end +-- Render the children function TypedSection.render_children(self, section) for k, node in ipairs(self.children) do - node.section = section - node:render() + node:render(section) end end +-- Return all matching UCI sections for this TypedSection function TypedSection.ucisections(self) local sections = {} for k, v in pairs(self.map.ucidata) do @@ -291,6 +418,7 @@ function TypedSection.ucisections(self) end + --[[ AbstractValue - An abstract Value Type null: Value can be empty @@ -298,47 +426,80 @@ AbstractValue - An abstract Value Type depends: A table of option => value pairs of which one must be true default: The default value size: The size of the input fields + rmempty: Unset value if empty + optional: This value is optional (see AbstractSection.optionals) ]]-- AbstractValue = class(Node) -function AbstractValue.__init__(self, option, ...) +function AbstractValue.__init__(self, map, option, ...) Node.__init__(self, ...) - self.option = option + self.option = option + self.map = map + self.config = map.config + self.tag_invalid = {} - self.valid = nil - self.depends = nil - self.default = nil - self.size = nil + self.valid = nil + self.depends = nil + self.default = nil + self.size = nil + self.optional = false end -function AbstractValue.formvalue(self) - local key = "cbid."..self.map.config.."."..self.section.."."..self.option +-- Returns the formvalue for this object +function AbstractValue.formvalue(self, section) + local key = "cbid."..self.map.config.."."..section.."."..self.option return ffluci.http.formvalue(key) end -function AbstractValue.parse(self) - local fvalue = self:formvalue() - if fvalue then +function AbstractValue.parse(self, section) + local fvalue = self:formvalue(section) + if fvalue == "" then + fvalue = nil + end + + + if fvalue then -- If we have a form value, validate it and write it to UCI fvalue = self:validate(fvalue) if not fvalue then - self.err_invalid = true + self.tag_invalid[section] = true end - if fvalue and not (fvalue == self:ucivalue()) then - self:write(fvalue) + if fvalue and not (fvalue == self:ucivalue(section)) then + self:write(section, fvalue) end + elseif ffluci.http.formvalue("cbi.submit") then -- Unset the UCI or error + if self.rmempty or self.optional then + self:remove(section) + else + self.tag_invalid[section] = true + end end end -function AbstractValue.ucivalue(self) - return self.map.ucidata[self.section][self.option] +-- Render if this value exists or if it is mandatory +function AbstractValue.render(self, section) + if not self.optional or self:ucivalue(section) then + ffluci.template.render(self.template, {self=self, section=section}) + end end +-- Return the UCI value of this object +function AbstractValue.ucivalue(self, section) + return self.map:get(section, self.option) +end + +-- Validate the form value function AbstractValue.validate(self, val) return ffluci.util.validate(val, self.valid) end -function AbstractValue.write(self, value) - return self.map:set(self.section, self.option, value) +-- Write to UCI +function AbstractValue.write(self, section, value) + return self.map:set(section, self.option, value) +end + +-- Remove from UCI +function AbstractValue.remove(self, section) + return self.map:del(section, self.option) end @@ -362,6 +523,7 @@ function Value.__init__(self, ...) self.isinteger = false end +-- This validation is a bit more complex function Value.validate(self, val) if self.maxlength and tostring(val):len() > self.maxlength then val = nil @@ -371,19 +533,123 @@ function Value.validate(self, val) end + +--[[ +Flag - A flag being enabled or disabled +]]-- +Flag = class(AbstractValue) + +function Flag.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/fvalue" + + self.enabled = "1" + self.disabled = "0" +end + +-- A flag can only have two states: set or unset +function Flag.parse(self, section) + self.default = self.enabled + local fvalue = self:formvalue(section) + + if fvalue then + fvalue = self.enabled + else + fvalue = self.disabled + end + + if fvalue == self.enabled or (not self.optional and not self.rmempty) then + if not(fvalue == self:ucivalue(section)) then + self:write(section, fvalue) + end + else + self:remove(section) + end +end + + + --[[ ListValue - A one-line value predefined in a list + widget: The widget that will be used (select, radio) ]]-- ListValue = class(AbstractValue) function ListValue.__init__(self, ...) AbstractValue.__init__(self, ...) self.template = "cbi/lvalue" + self.keylist = {} + self.vallist = {} - self.list = {} + self.size = 1 + self.widget = "select" end function ListValue.add_value(self, key, val) val = val or key - self.list[key] = val + table.insert(self.keylist, tostring(key)) + table.insert(self.vallist, tostring(val)) +end + +function ListValue.validate(self, val) + if ffluci.util.contains(self.keylist, val) then + return val + else + return nil + end +end + + + +--[[ +MultiValue - Multiple delimited values + widget: The widget that will be used (select, checkbox) + delimiter: The delimiter that will separate the values (default: " ") +]]-- +MultiValue = class(AbstractValue) + +function MultiValue.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/mvalue" + self.keylist = {} + self.vallist = {} + + self.widget = "checkbox" + self.delimiter = " " +end + +function MultiValue.add_value(self, key, val) + val = val or key + table.insert(self.keylist, tostring(key)) + table.insert(self.vallist, tostring(val)) +end + +function MultiValue.valuelist(self, section) + local val = self:ucivalue(section) + + if not(type(val) == "string") then + return {} + end + + return ffluci.util.split(val, self.delimiter) +end + +function MultiValue.validate(self, val) + if not(type(val) == "string") then + return nil + end + + local result = "" + + for value in val:gmatch("[^\n]+") do + if ffluci.util.contains(self.keylist, value) then + result = result .. self.delimiter .. value + end + end + + if result:len() > 0 then + return result:sub(self.delimiter:len() + 1) + else + return nil + end end \ No newline at end of file diff --git a/src/ffluci/init.lua b/src/ffluci/init.lua index 4585f51fb..dbecf57e4 100644 --- a/src/ffluci/init.lua +++ b/src/ffluci/init.lua @@ -25,7 +25,7 @@ limitations under the License. ]]-- module("ffluci", package.seeall) -__version__ = "0.1" +__version__ = "0.2" __appname__ = "FFLuCI" dispatch = require("ffluci.dispatcher").httpdispatch diff --git a/src/ffluci/util.lua b/src/ffluci/util.lua index caa8e41de..082310fbf 100644 --- a/src/ffluci/util.lua +++ b/src/ffluci/util.lua @@ -56,6 +56,23 @@ function class(base) end +-- Clones an object (deep on-demand) +function clone(object, deep) + local copy = {} + + for k, v in pairs(object) do + if deep and type(v) == "table" then + v = clone(v, deep) + end + copy[k] = v + end + + setmetatable(copy, getmetatable(object)) + + return copy +end + + -- Checks whether a table has an object "value" in it function contains(table, value) for k,v in pairs(table) do @@ -146,6 +163,28 @@ function sessionid() return ENV.SESSIONID end + +-- Splits a string into an array (Taken from lua-users.org) +function split(str, pat) + local t = {} + local fpat = "(.-)" .. pat + local last_end = 1 + local s, e, cap = str:find(fpat, 1) + while s do + if s ~= 1 or cap ~= "" then + table.insert(t,cap) + end + last_end = e+1 + s, e, cap = str:find(fpat, last_end) + end + if last_end <= #str then + cap = str:sub(last_end) + table.insert(t, cap) + end + return t +end + + -- Updates the scope of f with "extscope" function updfenv(f, extscope) local scope = getfenv(f) diff --git a/src/ffluci/view/cbi/fvalue.htm b/src/ffluci/view/cbi/fvalue.htm new file mode 100644 index 000000000..8c8a2dfba --- /dev/null +++ b/src/ffluci/view/cbi/fvalue.htm @@ -0,0 +1,10 @@ +
+
+
<%=self.title%>
+
<%=self.description%>
+
+
+ "<% if self:ucivalue(section) == self.enabled then %> checked="checked"<% end %> value="1" /> +
+
+
\ No newline at end of file diff --git a/src/ffluci/view/cbi/header.htm b/src/ffluci/view/cbi/header.htm index b11ed3b70..2731b6c2f 100644 --- a/src/ffluci/view/cbi/header.htm +++ b/src/ffluci/view/cbi/header.htm @@ -1,3 +1,4 @@ <%+header%>
"> + diff --git a/src/ffluci/view/cbi/lvalue.htm b/src/ffluci/view/cbi/lvalue.htm index 739d675c4..abe508017 100644 --- a/src/ffluci/view/cbi/lvalue.htm +++ b/src/ffluci/view/cbi/lvalue.htm @@ -1,11 +1,23 @@ -
-
<%=self.title%>
-
- "<% if self.size then %> size="<%=self.size%>"<% end %>> +<%for i, key in pairs(self.keylist) do%> + selected="selected"<% end %> value="<%=key%>"><%=self.vallist[i]%> <% end %> +<% elseif self.widget == "radio" then + local c = 0; + for i, key in pairs(self.keylist) do + c = c + 1%> + <%=self.vallist[i]%>"<% if self:ucivalue(section) == key then %> checked="checked"<% end %> value="<%=key%>" /> +<% if c == self.size then c = 0 %>
+<% end end %> +<% end %>
-
<%=self.description%>
+
\ No newline at end of file diff --git a/src/ffluci/view/cbi/mvalue.htm b/src/ffluci/view/cbi/mvalue.htm new file mode 100644 index 000000000..de7bd0c61 --- /dev/null +++ b/src/ffluci/view/cbi/mvalue.htm @@ -0,0 +1,26 @@ +<% +local v = self:valuelist(section) +%> +
+
+
<%=self.title%>
+
<%=self.description%>
+
+
+<% if self.widget == "select" then %> + +<% elseif self.widget == "checkbox" then + local c = 0; + for i, key in pairs(self.keylist) do + c = c + 1%> + <%=self.vallist[i]%>[]"<% if ffluci.util.contains(v, key) then %> checked="checked"<% end %> value="<%=key%>" /> +<% if c == self.size then c = 0 %>
+<% end end %> +<% end %> +
+
+
\ No newline at end of file diff --git a/src/ffluci/view/cbi/nsection.htm b/src/ffluci/view/cbi/nsection.htm index 84f893d2b..80dcefc07 100644 --- a/src/ffluci/view/cbi/nsection.htm +++ b/src/ffluci/view/cbi/nsection.htm @@ -1,7 +1,34 @@ -
+<% if self:ucivalue(self.section) then %> +

<%=self.title%>

-
<%=self.description%>
-
-<% self:render_children() %> -
+
<%=self.description%>
+
+<% self:render_children(self.section) %> + <% if #self.optionals > 0 or self.dynamic then %> +
+ <% if self.dynamic then %> + + <% else %> + + <% end %> + +
+ <% end %> +
+ <% if self.addremove then %> + + <% end %> +
+<% elseif self.addremove then %> +
+

<%=self.title%>

+
<%=self.description%>
+ +
+<% end %> diff --git a/src/ffluci/view/cbi/tsection.htm b/src/ffluci/view/cbi/tsection.htm index 987449406..26f8b198e 100644 --- a/src/ffluci/view/cbi/tsection.htm +++ b/src/ffluci/view/cbi/tsection.htm @@ -1,10 +1,26 @@ -
+

<%=self.title%>

-
<%=self.description%>
+
<%=self.description%>
<% for k, v in pairs(self:ucisections()) do%> -
+
<% if not self.anonymous then %><%=k%><% end %> <% self:render_children(k) %> + <% if #self.optionals > 0 or self.dynamic then %> +
+ <% if self.dynamic then %> + + <% else %> + + <% end %> + +
+ <% end %> +
<% if self.addremove then %> <% end %> @@ -12,12 +28,12 @@
<% end %> <% if self.addremove then %> -
+
<% if self.anonymous then %> - <% else %> + <% else %> - <% end %><% if self.err_invalid then %>
<%:cbi_invalid Fehler: Ungültiger Wert%>
<% end %> + <% end %><% if self.err_invalid then %>
<%:cbi_invalid Fehler: Ungültige Eingabe%>
<% end %>
<% end %>
diff --git a/src/ffluci/view/cbi/value.htm b/src/ffluci/view/cbi/value.htm index bbb5f5f3b..54ca720d9 100644 --- a/src/ffluci/view/cbi/value.htm +++ b/src/ffluci/view/cbi/value.htm @@ -1,8 +1,11 @@
-
<%=self.title%>
+
+
<%=self.title%>
+
<%=self.description%>
+
- size="<%=self.size%>" <% end %><% if self.maxlength then %>maxlength="<%=self.maxlength%>" <% end %>name="cbid.<%=self.config.."."..self.section.."."..self.option%>" value="<%=(self:ucivalue() or "")%>" /> + size="<%=self.size%>" <% end %><% if self.maxlength then %>maxlength="<%=self.maxlength%>" <% end %>name="cbid.<%=self.config.."."..section.."."..self.option%>" value="<%=(self:ucivalue(section) or "")%>" />
-
<%=self.description%>
- <% if self.err_invalid then %>
<%:cbi_invalid Fehler: Ungültiger Wert%>
<% end %> +
+ <% if self.tag_invalid[section] then %>
<%:cbi_invalid Fehler: Ungültige Eingabe%>
<% end %>
\ No newline at end of file -- 2.11.0