diff options
author | Steven Barth <steven@midlink.org> | 2008-04-11 18:13:58 +0000 |
---|---|---|
committer | Steven Barth <steven@midlink.org> | 2008-04-11 18:13:58 +0000 |
commit | cd498aa924553422e64c4b56d2fb01e63a170bac (patch) | |
tree | 8dc47490dff680dc2c67a0c30e0bd64e1b34d008 /core/src | |
parent | b864e2933ddab6bb40868cd878c9b89f9073ad12 (diff) |
* Major repository revision
Diffstat (limited to 'core/src')
-rw-r--r-- | core/src/ffluci/cbi.lua | 729 | ||||
-rw-r--r-- | core/src/ffluci/config.lua | 51 | ||||
-rw-r--r-- | core/src/ffluci/debug.lua | 2 | ||||
-rw-r--r-- | core/src/ffluci/dispatcher.lua | 257 | ||||
-rw-r--r-- | core/src/ffluci/fs.lua | 106 | ||||
-rw-r--r-- | core/src/ffluci/http.lua | 104 | ||||
-rw-r--r-- | core/src/ffluci/i18n.lua | 59 | ||||
-rw-r--r-- | core/src/ffluci/i18n/cbi.en | 4 | ||||
-rw-r--r-- | core/src/ffluci/init.lua | 33 | ||||
-rw-r--r-- | core/src/ffluci/menu.lua | 120 | ||||
-rw-r--r-- | core/src/ffluci/model/ipkg.lua | 140 | ||||
-rw-r--r-- | core/src/ffluci/model/uci.lua | 202 | ||||
-rw-r--r-- | core/src/ffluci/sys.lua | 126 | ||||
-rw-r--r-- | core/src/ffluci/template.lua | 229 | ||||
-rw-r--r-- | core/src/ffluci/util.lua | 208 |
15 files changed, 2370 insertions, 0 deletions
diff --git a/core/src/ffluci/cbi.lua b/core/src/ffluci/cbi.lua new file mode 100644 index 000000000..1ccf2e56d --- /dev/null +++ b/core/src/ffluci/cbi.lua @@ -0,0 +1,729 @@ +--[[ +FFLuCI - Configuration Bind Interface + +Description: +Offers an interface for binding confiugration values to certain +data types. Supports value and range validation and basic dependencies. + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- +module("ffluci.cbi", package.seeall) + +require("ffluci.template") +require("ffluci.util") +require("ffluci.http") +require("ffluci.model.uci") + +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") + require("ffluci.config") + + local cbidir = ffluci.config.path .. "/model/cbi/" + local func, err = loadfile(cbidir..cbimap..".lua") + + if not func then + return nil + end + + ffluci.i18n.loadc("cbi") + + ffluci.util.resfenv(func) + ffluci.util.updfenv(func, ffluci.cbi) + ffluci.util.extfenv(func, "translate", ffluci.i18n.translate) + + local map = func() + + if not instanceof(map, Map) then + error("CBI map returns no valid map object!") + return nil + end + + return map +end + +-- Node pseudo abstract class +Node = class() + +function Node.__init__(self, title, description) + self.children = {} + self.title = title or "" + self.description = description or "" + self.template = "cbi/node" +end + +-- Append child nodes +function Node.append(self, obj) + table.insert(self.children, obj) +end + +-- Parse this node and its children +function Node.parse(self, ...) + for k, child in ipairs(self.children) do + child:parse(...) + end +end + +-- Render this node +function Node.render(self) + ffluci.template.render(self.template, {self=self}) +end + +-- Render the children +function Node.render_children(self, ...) + for k, node in ipairs(self.children) do + node:render(...) + end +end + + +--[[ +Map - A map describing a configuration file +]]-- +Map = class(Node) + +function Map.__init__(self, config, ...) + Node.__init__(self, ...) + self.config = config + self.template = "cbi/map" + self.uci = ffluci.model.uci.Session() + self.ucidata = self.uci:show(self.config) + if not self.ucidata then + error("Unable to read UCI data: " .. self.config) + else + if not self.ucidata[self.config] then + self.ucidata[self.config] = {} + end + self.ucidata = self.ucidata[self.config] + end +end + +-- Creates a child section +function Map.section(self, class, ...) + if instanceof(class, AbstractSection) then + local obj = class(self, ...) + self:append(obj) + return obj + else + error("class must be a descendent of AbstractSection") + end +end + +-- UCI add +function Map.add(self, sectiontype) + local name = self.uci:add(self.config, sectiontype) + if name then + self.ucidata[name] = {} + self.ucidata[name][".type"] = sectiontype + end + return name +end + +-- UCI set +function Map.set(self, 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 + +-- 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 not section then + return self.ucidata + elseif 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, map, sectiontype, ...) + Node.__init__(self, ...) + self.sectiontype = sectiontype + self.map = map + self.config = map.config + self.optionals = {} + + self.optional = true + self.addremove = false + self.dynamic = false +end + +-- Appends a new option +function AbstractSection.option(self, class, ...) + if instanceof(class, AbstractValue) then + 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 + + self.optionals[section] = {} + + local field = ffluci.http.formvalue("cbi.opt."..self.config.."."..section) + for k,v in ipairs(self.children) do + if v.optional and not v:cfgvalue(section) then + if field == v.option then + field = nil + else + table.insert(self.optionals[section], 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:cfgvalue(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.cfgvalue(self, section) + return self.map:get(section) +end + +-- Removes the section +function AbstractSection.remove(self, section) + return self.map:del(section) +end + +-- Creates the section +function AbstractSection.create(self, section) + return self.map:set(section, nil, self.sectiontype) +end + + + +--[[ +NamedSection - A fixed configuration section defined by its name +]]-- +NamedSection = class(AbstractSection) + +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 s = self.section + local active = self:cfgvalue(s) + + + if self.addremove then + local path = self.config.."."..s + if active then -- Remove the section + if ffluci.http.formvalue("cbi.rns."..path) and self:remove(s) then + return + end + else -- Create and apply default values + if ffluci.http.formvalue("cbi.cns."..path) and self:create(s) then + for k,v in pairs(self.children) do + v:write(s, v.default) + end + end + end + end + + if active then + AbstractSection.parse_dynamic(self, s) + if ffluci.http.formvalue("cbi.submit") then + Node.parse(self, s) + end + AbstractSection.parse_optionals(self, s) + end +end + + +--[[ +TypedSection - A (set of) configuration section(s) defined by the type + addremove: Defines whether the user can add/remove sections of this type + anonymous: Allow creating anonymous sections + validate: a validation function returning nil if the section is invalid +]]-- +TypedSection = class(AbstractSection) + +function TypedSection.__init__(self, ...) + AbstractSection.__init__(self, ...) + self.template = "cbi/tsection" + self.deps = {} + self.excludes = {} + + self.anonymous = false +end + +-- Return all matching UCI sections for this TypedSection +function TypedSection.cfgsections(self) + local sections = {} + for k, v in pairs(self.map:get()) do + if v[".type"] == self.sectiontype then + if self:checkscope(k) then + sections[k] = v + end + end + end + return sections +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) + else + name = self.map:add(self.sectiontype) + end + + for k,v in pairs(self.children) do + if v.default then + self.map:set(name, v.option, v.default) + end + end +end + +-- Limits scope to sections that have certain option => value pairs +function TypedSection.depends(self, option, value) + table.insert(self.deps, {option=option, value=value}) +end + +-- Excludes several sections by name +function TypedSection.exclude(self, field) + self.excludes[field] = true +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 + if name then + self:create() + end + else + if name then + -- Ignore if it already exists + if self:cfgvalue(name) then + name = nil; + end + + name = self:checkscope(name) + + if not name then + self.err_invalid = true + end + + if name and name:len() > 0 then + self:create(name) + end + end + end + + -- Remove + crval = "cbi.rts." .. self.config + name = ffluci.http.formvalue(crval) + if type(name) == "table" then + for k,v in pairs(name) do + if self:cfgvalue(k) and self:checkscope(k) then + self:remove(k) + end + end + end + end + + for k, v in pairs(self:cfgsections()) do + AbstractSection.parse_dynamic(self, k) + if ffluci.http.formvalue("cbi.submit") then + Node.parse(self, k) + end + AbstractSection.parse_optionals(self, k) + end +end + +-- Render the children +function TypedSection.render_children(self, section) + for k, node in ipairs(self.children) do + node:render(section) + end +end + +-- Verifies scope of sections +function TypedSection.checkscope(self, section) + -- Check if we are not excluded + if self.excludes[section] then + return nil + end + + -- Check if at least one dependency is met + if #self.deps > 0 and self:cfgvalue(section) then + local stat = false + + for k, v in ipairs(self.deps) do + if self:cfgvalue(section)[v.option] == v.value then + stat = true + end + end + + if not stat then + return nil + end + end + + return self:validate(section) +end + + +-- Dummy validate function +function TypedSection.validate(self, section) + return section +end + + +--[[ +AbstractValue - An abstract Value Type + null: Value can be empty + valid: A function returning the value if it is valid otherwise nil + 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, map, option, ...) + Node.__init__(self, ...) + self.option = option + self.map = map + self.config = map.config + self.tag_invalid = {} + self.deps = {} + + self.rmempty = false + self.default = nil + self.size = nil + self.optional = false +end + +-- Add a dependencie to another section field +function AbstractValue.depends(self, field, value) + table.insert(self.deps, {field=field, value=value}) +end + +-- Return whether this object should be created +function AbstractValue.formcreated(self, section) + local key = "cbi.opt."..self.config.."."..section + return (ffluci.http.formvalue(key) == self.option) +end + +-- 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, section) + local fvalue = self:formvalue(section) + + if fvalue and fvalue ~= "" then -- If we have a form value, write it to UCI + fvalue = self:validate(fvalue) + if not fvalue then + self.tag_invalid[section] = true + end + if fvalue and not (fvalue == self:cfgvalue(section)) then + self:write(section, fvalue) + end + else -- Unset the UCI or error + if self.rmempty or self.optional then + self:remove(section) + end + end +end + +-- Render if this value exists or if it is mandatory +function AbstractValue.render(self, s) + if not self.optional or self:cfgvalue(s) or self:formcreated(s) then + ffluci.template.render(self.template, {self=self, section=s}) + end +end + +-- Return the UCI value of this object +function AbstractValue.cfgvalue(self, section) + return self.map:get(section, self.option) +end + +-- Validate the form value +function AbstractValue.validate(self, value) + return value +end + +-- 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 + + + + +--[[ +Value - A one-line value + maxlength: The maximum length + isnumber: The value must be a valid (floating point) number + isinteger: The value must be a valid integer + ispositive: The value must be positive (and a number) +]]-- +Value = class(AbstractValue) + +function Value.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/value" + + self.maxlength = nil + self.isnumber = false + 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 + end + + return ffluci.util.validate(val, self.isnumber, self.isinteger) +end + + +-- DummyValue - This does nothing except being there +DummyValue = class(AbstractValue) + +function DummyValue.__init__(self, map, ...) + AbstractValue.__init__(self, map, ...) + self.template = "cbi/dvalue" + self.value = nil +end + +function DummyValue.parse(self) + +end + +function DummyValue.render(self, s) + ffluci.template.render(self.template, {self=self, section=s}) +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) + 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:cfgvalue(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.size = 1 + self.widget = "select" +end + +function ListValue.value(self, key, val) + val = val or key + 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.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:cfgvalue(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/core/src/ffluci/config.lua b/core/src/ffluci/config.lua new file mode 100644 index 000000000..8b1a73dc7 --- /dev/null +++ b/core/src/ffluci/config.lua @@ -0,0 +1,51 @@ +--[[ +FFLuCI - Configuration + +Description: +Some FFLuCI configuration values read from uci file "luci" + + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +module("ffluci.config", package.seeall) +require("ffluci.model.uci") +require("ffluci.util") +require("ffluci.debug") + +-- Our path (wtf Lua lacks __file__ support) +path = ffluci.debug.path + +-- Warning! This is only for fallback and compatibility purporses! -- +main = {} + +-- This is where stylesheets and images go +main.mediaurlbase = "/ffluci/media" + +-- Does anybody think about browser autodetect here? +-- Too bad busybox doesn't populate HTTP_ACCEPT_LANGUAGE +main.lang = "de" + + +-- Now overwrite with UCI values +local ucidata = ffluci.model.uci.show("luci") +if ucidata and ucidata.luci then + ffluci.util.update(ffluci.config, ucidata.luci) +end
\ No newline at end of file diff --git a/core/src/ffluci/debug.lua b/core/src/ffluci/debug.lua new file mode 100644 index 000000000..f1132edcc --- /dev/null +++ b/core/src/ffluci/debug.lua @@ -0,0 +1,2 @@ +module("ffluci.debug", package.seeall) +path = require("ffluci.fs").dirname(debug.getinfo(1, 'S').source:sub(2))
\ No newline at end of file diff --git a/core/src/ffluci/dispatcher.lua b/core/src/ffluci/dispatcher.lua new file mode 100644 index 000000000..b60a9beef --- /dev/null +++ b/core/src/ffluci/dispatcher.lua @@ -0,0 +1,257 @@ +--[[ +FFLuCI - Dispatcher + +Description: +The request dispatcher and module dispatcher generators + + +The dispatching process: + For a detailed explanation of the dispatching process we assume: + You have installed the FFLuCI CGI-Dispatcher in /cgi-bin/ffluci + + To enforce a higher level of security only the CGI-Dispatcher + resides inside the web server's document root, everything else + stays inside an external directory, we assume this is /lua/ffluci + for this explanation. + + All controllers and action are reachable as sub-objects of /cgi-bin/ffluci + as if they were virtual folders and files + e.g.: /cgi-bin/ffluci/public/info/about + /cgi-bin/ffluci/admin/network/interfaces + and so on. + + The PATH_INFO variable holds the dispatch path and + will be split into three parts: /category/module/action + + Category: This is the category in which modules are stored in + By default there are two categories: + "public" - which is the default public category + "admin" - which is the default protected category + + As FFLuCI itself does not implement authentication + you should make sure that "admin" and other sensitive + categories are protected by the webserver. + + E.g. for busybox add a line like: + /cgi-bin/ffluci/admin:root:$p$root + to /etc/httpd.conf to protect the "admin" category + + + Module: This is the controller which will handle the request further + It is always a submodule of ffluci.controller, so a module + called "helloworld" will be stored in + /lua/ffluci/controller/helloworld.lua + You are free to submodule your controllers any further. + + Action: This is action that will be invoked after loading the module. + The kind of how the action will be dispatched depends on + the module dispatcher that is defined in the controller. + See the description of the default module dispatcher down + on this page for some examples. + + + The main dispatcher at first searches for the module by trying to + include ffluci.controller.category.module + (where "category" is the category name and "module" is the module name) + If this fails a 404 status code will be send to the client and FFLuCI exits + + Then the main dispatcher calls the module dispatcher + ffluci.controller.category.module.dispatcher with the request object + as the only argument. The module dispatcher is then responsible + for the further dispatching process. + + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +module("ffluci.dispatcher", package.seeall) +require("ffluci.http") +require("ffluci.template") +require("ffluci.config") +require("ffluci.sys") + + +-- Sets privilege for given category +function assign_privileges(category) + local cp = ffluci.config.category_privileges + if cp and cp[category] then + local u, g = cp[category]:match("([^:]+):([^:]+)") + ffluci.sys.process.setuser(u) + ffluci.sys.process.setgroup(g) + end +end + +-- Dispatches the "request" +function dispatch(req) + request = req + local m = "ffluci.controller." .. request.category .. "." .. request.module + local stat, module = pcall(require, m) + if not stat then + return error404() + else + module.request = request + module.dispatcher = module.dispatcher or dynamic + setfenv(module.dispatcher, module) + return module.dispatcher(request) + end +end + +-- Sends a 404 error code and renders the "error404" template if available +function error404(message) + message = message or "Not Found" + + if not pcall(ffluci.template.render, "error404") then + ffluci.http.textheader() + print(message) + end + return false +end + +-- Sends a 500 error code and renders the "error500" template if available +function error500(message) + ffluci.http.status(500, "Internal Server Error") + + if not pcall(ffluci.template.render, "error500", {message=message}) then + ffluci.http.textheader() + print(message) + end + return false +end + + +-- Dispatches a request depending on the PATH_INFO variable +function httpdispatch() + local pathinfo = os.getenv("PATH_INFO") or "" + local parts = pathinfo:gmatch("/[%w-]+") + + local sanitize = function(s, default) + return s and s:sub(2) or default + end + + local cat = sanitize(parts(), "public") + local mod = sanitize(parts(), "index") + local act = sanitize(parts(), "index") + + assign_privileges(cat) + dispatch({category=cat, module=mod, action=act}) +end + + +-- Dispatchers -- + + +-- The Action Dispatcher searches the module for any function called +-- action_"request.action" and calls it +function action(request) + local i18n = require("ffluci.i18n") + local disp = require("ffluci.dispatcher") + + i18n.loadc(request.module) + local action = getfenv()["action_" .. request.action:gsub("-", "_")] + if action then + action() + else + disp.error404() + end +end + +-- The CBI dispatcher directly parses and renders the CBI map which is +-- placed in ffluci/modles/cbi/"request.module"/"request.action" +function cbi(request) + local i18n = require("ffluci.i18n") + local disp = require("ffluci.dispatcher") + local tmpl = require("ffluci.template") + local cbi = require("ffluci.cbi") + + local path = request.category.."_"..request.module.."/"..request.action + + i18n.loadc(request.module) + + local stat, map = pcall(cbi.load, path) + if stat and map then + local stat, err = pcall(map.parse, map) + if not stat then + disp.error500(err) + return + end + tmpl.render("cbi/header") + map:render() + tmpl.render("cbi/footer") + elseif not stat then + disp.error500(map) + else + disp.error404() + end +end + +-- The dynamic dispatchers combines the action, simpleview and cbi dispatchers +-- in one dispatcher. It tries to lookup the request in this order. +function dynamic(request) + local i18n = require("ffluci.i18n") + local disp = require("ffluci.dispatcher") + local tmpl = require("ffluci.template") + local cbi = require("ffluci.cbi") + + i18n.loadc(request.module) + + local action = getfenv()["action_" .. request.action:gsub("-", "_")] + if action then + action() + return + end + + local path = request.category.."_"..request.module.."/"..request.action + if pcall(tmpl.render, path) then + return + end + + local stat, map = pcall(cbi.load, path) + if stat and map then + local stat, err = pcall(map.parse, map) + if not stat then + disp.error500(err) + return + end + tmpl.render("cbi/header") + map:render() + tmpl.render("cbi/footer") + return + elseif not stat then + disp.error500(map) + return + end + + disp.error404() +end + +-- The Simple View Dispatcher directly renders the template +-- which is placed in ffluci/views/"request.module"/"request.action" +function simpleview(request) + local i18n = require("ffluci.i18n") + local tmpl = require("ffluci.template") + local disp = require("ffluci.dispatcher") + + local path = request.category.."_"..request.module.."/"..request.action + + i18n.loadc(request.module) + if not pcall(tmpl.render, path) then + disp.error404() + end +end
\ No newline at end of file diff --git a/core/src/ffluci/fs.lua b/core/src/ffluci/fs.lua new file mode 100644 index 000000000..6e8859a0d --- /dev/null +++ b/core/src/ffluci/fs.lua @@ -0,0 +1,106 @@ +--[[ +FFLuCI - Filesystem tools + +Description: +A module offering often needed filesystem manipulation functions + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +module("ffluci.fs", package.seeall) + +require("posix") + +-- Checks whether a file exists +function isfile(filename) + local fp = io.open(path, "r") + if file then file:close() end + return file ~= nil +end + +-- Returns the content of file +function readfile(filename) + local fp, err = io.open(filename) + + if fp == nil then + return nil, err + end + + local data = fp:read("*a") + fp:close() + return data +end + +-- Returns the content of file as array of lines +function readfilel(filename) + local fp, err = io.open(filename) + local line = "" + local data = {} + + if fp == nil then + return nil, err + end + + while true do + line = fp:read() + if (line == nil) then break end + table.insert(data, line) + end + + fp:close() + return data +end + +-- Writes given data to a file +function writefile(filename, data) + local fp, err = io.open(filename, "w") + + if fp == nil then + return nil, err + end + + fp:write(data) + fp:close() + + return true +end + +-- Returns the file modification date/time of "path" +function mtime(path) + return posix.stat(path, "mtime") +end + +-- basename wrapper +basename = posix.basename + +-- dirname wrapper +dirname = posix.dirname + +-- dir wrapper +function dir(path) + local dir = {} + for node in posix.files(path) do + table.insert(dir, 1, node) + end + return dir +end + +-- Alias for lfs.mkdir +mkdir = posix.mkdir
\ No newline at end of file diff --git a/core/src/ffluci/http.lua b/core/src/ffluci/http.lua new file mode 100644 index 000000000..06e1c43bd --- /dev/null +++ b/core/src/ffluci/http.lua @@ -0,0 +1,104 @@ +--[[ +FFLuCI - HTTP-Interaction + +Description: +HTTP-Header manipulator and form variable preprocessor + +FileId: +$Id$ + +ToDo: +- Cookie handling + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +module("ffluci.http", package.seeall) + +require("ffluci.util") + +-- Sets HTTP-Status-Header +function status(code, message) + print("Status: " .. tostring(code) .. " " .. message) +end + + +-- Asks the browser to redirect to "url" +function redirect(url, qs) + if qs then + url = url .. "?" .. qs + end + + status(302, "Found") + print("Location: " .. url .. "\n") +end + + +-- Same as redirect but accepts category, module and action for internal use +function request_redirect(category, module, action, ...) + category = category or "public" + module = module or "index" + action = action or "index" + + local pattern = script_name() .. "/%s/%s/%s" + redirect(pattern:format(category, module, action), ...) +end + + +-- Returns the script name +function script_name() + return ENV.SCRIPT_NAME +end + + +-- Gets form value from key +function formvalue(key, default) + local c = formvalues() + + for match in key:gmatch("[%w-_]+") do + c = c[match] + if c == nil then + return default + end + end + + return c +end + + +-- Returns a table of all COOKIE, GET and POST Parameters +function formvalues() + return FORM +end + + +-- Prints plaintext content-type header +function textheader() + print("Content-Type: text/plain\n") +end + + +-- Prints html content-type header +function htmlheader() + print("Content-Type: text/html\n") +end + + +-- Prints xml content-type header +function xmlheader() + print("Content-Type: text/xml\n") +end diff --git a/core/src/ffluci/i18n.lua b/core/src/ffluci/i18n.lua new file mode 100644 index 000000000..11f4afe87 --- /dev/null +++ b/core/src/ffluci/i18n.lua @@ -0,0 +1,59 @@ +--[[ +FFLuCI - Internationalisation + +Description: +A very minimalistic but yet effective internationalisation module + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +module("ffluci.i18n", package.seeall) + +require("ffluci.config") + +table = {} +i18ndir = ffluci.config.path .. "/i18n/" + +-- Clears the translation table +function clear() + table = {} +end + +-- Loads a translation and copies its data into the global translation table +function load(file) + local f = loadfile(i18ndir .. file) + if f then + setfenv(f, table) + f() + return true + else + return false + end +end + +-- Same as load but autocompletes the filename with .LANG from config.lang +function loadc(file) + return load(file .. "." .. ffluci.config.main.lang) +end + +-- Returns the i18n-value defined by "key" or if there is no such: "default" +function translate(key, default) + return table[key] or default +end
\ No newline at end of file diff --git a/core/src/ffluci/i18n/cbi.en b/core/src/ffluci/i18n/cbi.en new file mode 100644 index 000000000..7c159ce50 --- /dev/null +++ b/core/src/ffluci/i18n/cbi.en @@ -0,0 +1,4 @@ +uci_add = "Add entry" +uci_del = "Remove entry" +uci_save = "Save configuration" +uci_reset = "Reset form"
\ No newline at end of file diff --git a/core/src/ffluci/init.lua b/core/src/ffluci/init.lua new file mode 100644 index 000000000..dbecf57e4 --- /dev/null +++ b/core/src/ffluci/init.lua @@ -0,0 +1,33 @@ +--[[ +FFLuCI - Freifunk Lua Configuration Interface + +Description: +This is the init file + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- +module("ffluci", package.seeall) + +__version__ = "0.2" +__appname__ = "FFLuCI" + +dispatch = require("ffluci.dispatcher").httpdispatch +env = ENV +form = FORM diff --git a/core/src/ffluci/menu.lua b/core/src/ffluci/menu.lua new file mode 100644 index 000000000..0a1aad5d1 --- /dev/null +++ b/core/src/ffluci/menu.lua @@ -0,0 +1,120 @@ +--[[ +FFLuCI - Menu Builder + +Description: +Collects menu building information from controllers + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- +module("ffluci.menu", package.seeall) + +require("ffluci.fs") +require("ffluci.util") +require("ffluci.template") +require("ffluci.i18n") +require("ffluci.config") + +-- Default modelpath +modelpath = ffluci.config.path .. "/model/menu/" + +-- Menu definition extra scope +scope = { + translate = ffluci.i18n.translate +} + +-- Local menu database +local menu = {} + +-- The current pointer +local menuc = {} + +-- Adds a menu category to the current menu and selects it +function add(cat, controller, title, order) + order = order or 100 + if not menu[cat] then + menu[cat] = {} + end + + local entry = {} + entry[".descr"] = title + entry[".order"] = order + entry[".contr"] = controller + + menuc = entry + + local i = 0 + for k,v in ipairs(menu[cat]) do + if v[".order"] > entry[".order"] then + break + end + i = k + end + table.insert(menu[cat], i+1, entry) + + return true +end + +-- Adds an action to the current menu +function act(action, title) + table.insert(menuc, {action = action, descr = title}) + return true +end + +-- Selects a menu category +function sel(cat, controller) + if not menu[cat] then + return nil + end + menuc = menu[cat] + + local stat = nil + for k,v in ipairs(menuc) do + if v[".contr"] == controller then + menuc = v + stat = true + end + end + + return stat +end + + +-- Collect all menu information provided in the model dir +function collect() + for k, menu in pairs(ffluci.fs.dir(modelpath)) do + if menu:sub(1, 1) ~= "." then + local f = loadfile(modelpath.."/"..menu) + local env = ffluci.util.clone(scope) + + env.add = add + env.sel = sel + env.act = act + + setfenv(f, env) + f() + end + end +end + +-- Returns the menu information +function get() + collect() + return menu +end
\ No newline at end of file diff --git a/core/src/ffluci/model/ipkg.lua b/core/src/ffluci/model/ipkg.lua new file mode 100644 index 000000000..3b149fb16 --- /dev/null +++ b/core/src/ffluci/model/ipkg.lua @@ -0,0 +1,140 @@ +--[[ +FFLuCI - IPKG wrapper library + +Description: +Wrapper for the ipkg Package manager + +Any return value of false or nil can be interpreted as an error + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- +module("ffluci.model.ipkg", package.seeall) +require("ffluci.sys") +require("ffluci.util") + +ipkg = "ipkg" + +-- Returns repository information +function info(pkg) + return _lookup("info", pkg) +end + +-- Returns a table with status information +function status(pkg) + return _lookup("status", pkg) +end + +-- Installs packages +function install(...) + return _action("install", ...) +end + +-- Returns whether a package is installed +function installed(pkg, ...) + local p = status(...)[pkg] + return (p and p.Status and p.Status.installed) +end + +-- Removes packages +function remove(...) + return _action("remove", ...) +end + +-- Updates package lists +function update() + return _action("update") +end + +-- Upgrades installed packages +function upgrade() + return _action("upgrade") +end + + +-- Internal action function +function _action(cmd, ...) + local pkg = "" + arg.n = nil + for k, v in pairs(arg) do + pkg = pkg .. " '" .. v:gsub("'", "") .. "'" + end + + local c = ipkg.." "..cmd.." "..pkg.." >/dev/null 2>&1" + local r = os.execute(c) + return (r == 0), r +end + +-- Internal lookup function +function _lookup(act, pkg) + local cmd = ipkg .. " " .. act + if pkg then + cmd = cmd .. " '" .. pkg:gsub("'", "") .. "'" + end + + return _parselist(ffluci.sys.exec(cmd .. " 2>/dev/null")) +end + +-- Internal parser function +function _parselist(rawdata) + if type(rawdata) ~= "string" then + error("IPKG: Invalid rawdata given") + end + + rawdata = ffluci.util.split(rawdata) + local data = {} + local c = {} + local l = nil + + for k, line in pairs(rawdata) do + if line:sub(1, 1) ~= " " then + local split = ffluci.util.split(line, ":", 1) + local key = nil + local val = nil + + if split[1] then + key = ffluci.util.trim(split[1]) + end + + if split[2] then + val = ffluci.util.trim(split[2]) + end + + if key and val then + if key == "Package" then + c = {Package = val} + data[val] = c + elseif key == "Status" then + c.Status = {} + for i, j in pairs(ffluci.util.split(val, " ")) do + c.Status[j] = true + end + else + c[key] = val + end + l = key + end + else + -- Multi-line field + c[l] = c[l] .. "\n" .. line:sub(2) + end + end + + return data +end
\ No newline at end of file diff --git a/core/src/ffluci/model/uci.lua b/core/src/ffluci/model/uci.lua new file mode 100644 index 000000000..828659780 --- /dev/null +++ b/core/src/ffluci/model/uci.lua @@ -0,0 +1,202 @@ +--[[ +FFLuCI - UCI wrapper library + +Description: +Wrapper for the /sbin/uci application, syntax of implemented functions +is comparable to the syntax of the uci application + +Any return value of false or nil can be interpreted as an error + + +ToDo: Reimplement in Lua + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- +module("ffluci.model.uci", package.seeall) +require("ffluci.util") +require("ffluci.fs") +require("ffluci.sys") + +-- The OS uci command +ucicmd = "uci" + +-- Session class +Session = ffluci.util.class() + +-- Session constructor +function Session.__init__(self, path, uci) + uci = uci or ucicmd + if path then + self.ucicmd = uci .. " -P " .. path + else + self.ucicmd = uci + end +end + +-- The default Session +local default = Session() + +-- Wrapper for "uci add" +function Session.add(self, config, section_type) + return self:_uci("add " .. _path(config) .. " " .. _path(section_type)) +end + +function add(...) + return default:add(...) +end + + +-- Wrapper for "uci changes" +function Session.changes(self, config) + return self:_uci("changes " .. _path(config)) +end + +function changes(...) + return default:changes(...) +end + + +-- Wrapper for "uci commit" +function Session.commit(self, config) + return self:_uci2("commit " .. _path(config)) +end + +function commit(...) + return default:commit(...) +end + + +-- Wrapper for "uci del" +function Session.del(self, config, section, option) + return self:_uci2("del " .. _path(config, section, option)) +end + +function del(...) + return default:del(...) +end + + +-- Wrapper for "uci get" +function Session.get(self, config, section, option) + return self:_uci("get " .. _path(config, section, option)) +end + +function get(...) + return default:get(...) +end + + +-- Wrapper for "uci revert" +function Session.revert(self, config) + return self:_uci2("revert " .. _path(config)) +end + +function revert(...) + return default:revert(...) +end + + +-- Wrapper for "uci show" +function Session.show(self, config) + return self:_uci3("show " .. _path(config)) +end + +function show(...) + return default:show(...) +end + + +-- Wrapper for "uci set" +function Session.set(self, config, section, option, value) + return self:_uci2("set " .. _path(config, section, option, value)) +end + +function set(...) + return default:set(...) +end + + +-- Internal functions -- + +function Session._uci(self, cmd) + local res = ffluci.sys.exec(self.ucicmd .. " 2>/dev/null " .. cmd) + + if res:len() == 0 then + return nil + else + return res:sub(1, res:len()-1) + end +end + +function Session._uci2(self, cmd) + local res = ffluci.sys.exec(self.ucicmd .. " 2>&1 " .. cmd) + + if res:len() > 0 then + return false, res + else + return true + end +end + +function Session._uci3(self, cmd) + local res = ffluci.sys.execl(self.ucicmd .. " 2>&1 " .. cmd) + if res[1] and res[1]:sub(1, self.ucicmd:len()+1) == self.ucicmd..":" then + return nil, res[1] + end + + table = {} + + for k,line in pairs(res) do + c, s, t = line:match("^([^.]-)%.([^.]-)=(.-)$") + if c then + table[c] = table[c] or {} + table[c][s] = {} + table[c][s][".type"] = t + end + + c, s, o, v = line:match("^([^.]-)%.([^.]-)%.([^.]-)=(.-)$") + if c then + table[c][s][o] = v + end + end + + return table +end + +-- Build path (config.section.option=value) and prevent command injection +function _path(...) + local result = "" + + -- Not using ipairs because it is not reliable in case of nil arguments + arg.n = nil + for k,v in pairs(arg) do + if v then + v = tostring(v) + if k == 1 then + result = "'" .. v:gsub("['.]", "") .. "'" + elseif k < 4 then + result = result .. ".'" .. v:gsub("['.]", "") .. "'" + elseif k == 4 then + result = result .. "='" .. v:gsub("'", "") .. "'" + end + end + end + return result +end
\ No newline at end of file diff --git a/core/src/ffluci/sys.lua b/core/src/ffluci/sys.lua new file mode 100644 index 000000000..d8fbaa57a --- /dev/null +++ b/core/src/ffluci/sys.lua @@ -0,0 +1,126 @@ +--[[ +FFLuCI - System library + +Description: +Utilities for interaction with the Linux system + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +module("ffluci.sys", package.seeall) +require("posix") + +-- Runs "command" and returns its output +function exec(command) + local pp = io.popen(command) + local data = pp:read("*a") + pp:close() + + return data +end + +-- Runs "command" and returns its output as a array of lines +function execl(command) + local pp = io.popen(command) + local line = "" + local data = {} + + while true do + line = pp:read() + if (line == nil) then break end + table.insert(data, line) + end + pp:close() + + return data +end + +-- Uses "ffluci-flash" to flash a new image file to the system +function flash(image, kpattern) + local cmd = "ffluci-flash " + if kpattern then + cmd = cmd .. "-k '" .. kapttern:gsub("'", "") .. "' " + end + cmd = cmd .. "'" .. image:gsub("'", "") .. "'" + + return os.execute(cmd) +end + +-- Returns the hostname +function hostname() + return io.lines("/proc/sys/kernel/hostname")() +end + +-- Returns the load average +function loadavg() + local loadavg = io.lines("/proc/loadavg")() + return loadavg:match("^(.-) (.-) (.-) (.-) (.-)$") +end + +-- Reboots the system +function reboot() + return os.execute("reboot >/dev/null 2>&1") +end + + +group = {} +group.getgroup = posix.getgroup + +net = {} +-- Returns all available network interfaces +function net.devices() + local devices = {} + for line in io.lines("/proc/net/dev") do + table.insert(devices, line:match(" *(.-):")) + end + return devices +end + +process = {} +process.info = posix.getpid + +-- Sets the gid of a process +function process.setgroup(pid, gid) + return posix.setpid("g", pid, gid) +end + +-- Sets the uid of a process +function process.setuser(pid, uid) + return posix.setpid("u", pid, uid) +end + +user = {} +-- returns user information to a given uid +user.getuser = posix.getpasswd + +-- Changes the user password of given user +function user.setpasswd(user, pwd) + if pwd then + pwd = pwd:gsub("'", "") + end + + if user then + user = user:gsub("'", "") + end + + local cmd = "(echo '"..pwd.."';sleep 1;echo '"..pwd.."')|" + cmd = cmd .. "passwd '"..user.."' >/dev/null 2>&1" + return os.execute(cmd) +end
\ No newline at end of file diff --git a/core/src/ffluci/template.lua b/core/src/ffluci/template.lua new file mode 100644 index 000000000..502013684 --- /dev/null +++ b/core/src/ffluci/template.lua @@ -0,0 +1,229 @@ +--[[ +FFLuCI - Template Parser + +Description: +A template parser supporting includes, translations, Lua code blocks +and more. It can be used either as a compiler or as an interpreter. + +FileId: $Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- +module("ffluci.template", package.seeall) + +require("ffluci.config") +require("ffluci.util") +require("ffluci.fs") +require("ffluci.i18n") +require("ffluci.http") +require("ffluci.model.uci") + +viewdir = ffluci.config.path .. "/view/" + + +-- Compile modes: +-- none: Never compile, only use precompiled data from files +-- memory: Always compile, do not save compiled files, ignore precompiled +-- file: Compile on demand, save compiled files, update precompiled +compiler_mode = "memory" + + +-- This applies to compiler modes "always" and "smart" +-- +-- Produce compiled lua code rather than lua sourcecode +-- WARNING: Increases template size heavily!!! +-- This produces the same bytecode as luac but does not have a strip option +compiler_enable_bytecode = false + + +-- Define the namespace for template modules +viewns = { + translate = ffluci.i18n.translate, + config = function(...) return ffluci.model.uci.get(...) or "" end, + controller = ffluci.http.script_name(), + media = ffluci.config.main.mediaurlbase, + write = io.write, + include = function(name) Template(name):render(getfenv(2)) end, +} + +-- Compiles a given template into an executable Lua module +function compile(template) + -- Search all <% %> expressions (remember: Lua table indexes begin with #1) + local function expr_add(command) + table.insert(expr, command) + return "<%" .. tostring(#expr) .. "%>" + end + + -- As "expr" should be local, we have to assign it to the "expr_add" scope + local expr = {} + ffluci.util.extfenv(expr_add, "expr", expr) + + -- Save all expressiosn to table "expr" + template = template:gsub("<%%(.-)%%>", expr_add) + + local function sanitize(s) + s = ffluci.util.escape(s) + s = ffluci.util.escape(s, "'") + s = ffluci.util.escape(s, "\n") + return s + end + + -- Escape and sanitize all the template (all non-expressions) + template = sanitize(template) + + -- Template module header/footer declaration + local header = "write('" + local footer = "')" + + template = header .. template .. footer + + -- Replacements + local r_include = "')\ninclude('%s')\nwrite('" + local r_i18n = "'..translate('%1','%2')..'" + local r_uci = "'..config('%1','%2','%3')..'" + local r_pexec = "'..%s..'" + local r_exec = "')\n%s\nwrite('" + + -- Parse the expressions + for k,v in pairs(expr) do + local p = v:sub(1, 1) + local re = nil + if p == "+" then + re = r_include:format(sanitize(string.sub(v, 2))) + elseif p == ":" then + re = sanitize(v):gsub(":(.-) (.+)", r_i18n) + elseif p == "~" then + re = sanitize(v):gsub("~(.-)%.(.-)%.(.+)", r_uci) + elseif p == "=" then + re = r_pexec:format(v:sub(2)) + else + re = r_exec:format(v) + end + template = template:gsub("<%%"..tostring(k).."%%>", re) + end + + if compiler_enable_bytecode then + tf = loadstring(template) + template = string.dump(tf) + end + + return template +end + +-- Oldstyle render shortcut +function render(name, scope, ...) + scope = scope or getfenv(2) + local s, t = pcall(Template, name) + if not s then + error(t) + else + t:render(scope, ...) + end +end + + +-- Template class +Template = ffluci.util.class() + +-- Shared template cache to store templates in to avoid unnecessary reloading +Template.cache = {} + + +-- Constructor - Reads and compiles the template on-demand +function Template.__init__(self, name) + if self.cache[name] then + self.template = self.cache[name] + else + self.template = nil + end + + -- Create a new namespace for this template + self.viewns = {} + + -- Copy over from general namespace + for k, v in pairs(viewns) do + self.viewns[k] = v + end + + -- If we have a cached template, skip compiling and loading + if self.template then + return + end + + -- Compile and build + local sourcefile = viewdir .. name .. ".htm" + local compiledfile = viewdir .. name .. ".lua" + local err + + if compiler_mode == "file" then + local tplmt = ffluci.fs.mtime(sourcefile) + local commt = ffluci.fs.mtime(compiledfile) + + -- Build if there is no compiled file or if compiled file is outdated + if ((commt == nil) and not (tplmt == nil)) + or (not (commt == nil) and not (tplmt == nil) and commt < tplmt) then + local source + source, err = ffluci.fs.readfile(sourcefile) + + if source then + local compiled = compile(source) + ffluci.fs.writefile(compiledfile, compiled) + self.template, err = loadstring(compiled) + end + else + self.template, err = loadfile(compiledfile) + end + + elseif compiler_mode == "none" then + self.template, err = loadfile(self.compiledfile) + + elseif compiler_mode == "memory" then + local source + source, err = ffluci.fs.readfile(sourcefile) + if source then + self.template, err = loadstring(compile(source)) + end + + end + + -- If we have no valid template throw error, otherwise cache the template + if not self.template then + error(err) + else + self.cache[name] = self.template + end +end + + +-- Renders a template +function Template.render(self, scope) + scope = scope or getfenv(2) + + -- Save old environment + local oldfenv = getfenv(self.template) + + -- Put our predefined objects in the scope of the template + ffluci.util.resfenv(self.template) + ffluci.util.updfenv(self.template, scope) + ffluci.util.updfenv(self.template, self.viewns) + + -- Now finally render the thing + self.template() + + -- Reset environment + setfenv(self.template, oldfenv) +end diff --git a/core/src/ffluci/util.lua b/core/src/ffluci/util.lua new file mode 100644 index 000000000..dfc88e3e4 --- /dev/null +++ b/core/src/ffluci/util.lua @@ -0,0 +1,208 @@ +--[[ +FFLuCI - Utility library + +Description: +Several common useful Lua functions + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +module("ffluci.util", package.seeall) + + +-- Lua simplified Python-style OO class support emulation +function class(base) + local class = {} + + local create = function(class, ...) + local inst = {} + setmetatable(inst, {__index = class}) + + if inst.__init__ then + local stat, err = pcall(inst.__init__, inst, ...) + if not stat then + error(err) + end + end + + return inst + end + + local classmeta = {__call = create} + + if base then + classmeta.__index = base + end + + setmetatable(class, classmeta) + return class +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 + if value == v then + return true + end + end + return false +end + + +-- Dumps a table to stdout (useful for testing and debugging) +function dumptable(t, i) + i = i or 0 + for k,v in pairs(t) do + print(string.rep("\t", i) .. k, v) + if type(v) == "table" then + dumptable(v, i+1) + end + end +end + + +-- Escapes all occurences of c in s +function escape(s, c) + c = c or "\\" + return s:gsub(c, "\\" .. c) +end + + +-- Populate obj in the scope of f as key +function extfenv(f, key, obj) + local scope = getfenv(f) + scope[key] = obj +end + + +-- Checks whether an object is an instanceof class +function instanceof(object, class) + local meta = getmetatable(object) + while meta and meta.__index do + if meta.__index == class then + return true + end + meta = getmetatable(meta.__index) + end + return false +end + + +-- Creates valid XML PCDATA from a string +function pcdata(value) + value = value:gsub("&", "&") + value = value:gsub('"', """) + value = value:gsub("'", "'") + value = value:gsub("<", "<") + return value:gsub(">", ">") +end + + +-- Resets the scope of f doing a shallow copy of its scope into a new table +function resfenv(f) + setfenv(f, clone(getfenv(f))) +end + + +-- Returns the Haserl unique sessionid +function sessionid() + return ENV.SESSIONID +end + + +-- Splits a string into an array (Adapted from lua-users.org) +function split(str, pat, max) + pat = pat or "\n" + max = max or -1 + + local t = {} + local fpat = "(.-)" .. pat + local last_end = 1 + local s, e, cap = str:find(fpat, 1) + + while s do + max = max - 1 + if s ~= 1 or cap ~= "" then + table.insert(t,cap) + end + last_end = e+1 + if max == 0 then + break + end + 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 + +-- Removes whitespace from beginning and end of a string +function trim (string) + return string:gsub("^%s*(.-)%s*$", "%1") +end + +-- Updates given table with new values +function update(t, updates) + for k, v in pairs(updates) do + t[k] = v + end +end + + +-- Updates the scope of f with "extscope" +function updfenv(f, extscope) + update(getfenv(f), extscope) +end + + +-- Validates a variable +function validate(value, cast_number, cast_int) + if cast_number or cast_int then + value = tonumber(value) + end + + if cast_int and value and not(value % 1 == 0) then + value = nil + end + + return value +end
\ No newline at end of file |