summaryrefslogtreecommitdiffhomepage
path: root/core
diff options
context:
space:
mode:
authorSteven Barth <steven@midlink.org>2008-04-11 18:13:58 +0000
committerSteven Barth <steven@midlink.org>2008-04-11 18:13:58 +0000
commitcd498aa924553422e64c4b56d2fb01e63a170bac (patch)
tree8dc47490dff680dc2c67a0c30e0bd64e1b34d008 /core
parentb864e2933ddab6bb40868cd878c9b89f9073ad12 (diff)
* Major repository revision
Diffstat (limited to 'core')
-rw-r--r--core/Makefile37
-rw-r--r--core/contrib/uci/luci35
-rwxr-xr-xcore/ffluci5
-rwxr-xr-xcore/ffluci-upload4
-rwxr-xr-xcore/index.cgi3
-rw-r--r--core/index.html10
-rw-r--r--core/src/ffluci/cbi.lua729
-rw-r--r--core/src/ffluci/config.lua51
-rw-r--r--core/src/ffluci/debug.lua2
-rw-r--r--core/src/ffluci/dispatcher.lua257
-rw-r--r--core/src/ffluci/fs.lua106
-rw-r--r--core/src/ffluci/http.lua104
-rw-r--r--core/src/ffluci/i18n.lua59
-rw-r--r--core/src/ffluci/i18n/cbi.en4
-rw-r--r--core/src/ffluci/init.lua33
-rw-r--r--core/src/ffluci/menu.lua120
-rw-r--r--core/src/ffluci/model/ipkg.lua140
-rw-r--r--core/src/ffluci/model/uci.lua202
-rw-r--r--core/src/ffluci/sys.lua126
-rw-r--r--core/src/ffluci/template.lua229
-rw-r--r--core/src/ffluci/util.lua208
21 files changed, 2464 insertions, 0 deletions
diff --git a/core/Makefile b/core/Makefile
new file mode 100644
index 000000000..7db8ddb4a
--- /dev/null
+++ b/core/Makefile
@@ -0,0 +1,37 @@
+LUAC = luac
+LUAC_OPTIONS = -s
+
+FILES = ffluci/debug.lua ffluci/view/*.htm ffluci/view/cbi/*.htm
+
+CFILES = ffluci/util.lua ffluci/http.lua ffluci/fs.lua \
+ffluci/sys.lua ffluci/model/uci.lua ffluci/model/ipkg.lua \
+ffluci/config.lua ffluci/i18n.lua ffluci/template.lua \
+ffluci/cbi.lua ffluci/dispatcher.lua ffluci/menu.lua ffluci/init.lua
+
+DIRECTORIES = ffluci/model/cbi ffluci/model/menu ffluci/controller ffluci/i18n ffluci/view/cbi
+
+OUTDIRS = $(DIRECTORIES:%=dist/%)
+INFILES = $(CFILES:%=src/%)
+OUTFILE = ffluci/init.lua
+CPFILES = $(FILES:%=src/%)
+
+.PHONY: all compile source depends clean
+
+all: compile
+
+depends:
+ mkdir -p $(OUTDIRS)
+ for i in $(CPFILES); do [ -f "$$i" ] && (i=$$(echo $$i | cut -d/ -f2-); \
+ mkdir -p dist/$$(dirname $$i); cp src/$$i dist/$$i); done
+
+compile: depends
+ $(LUAC) $(LUAC_OPTIONS) -o dist/$(OUTFILE) $(INFILES)
+ for i in $(CFILES); do [ -f dist/$$i ] || ln -s `dirname $$i | cut -s -d / -f 2- | sed -e 's/[^/]*\/*/..\//g'``basename $(OUTFILE)` dist/$$i; done
+
+
+source: depends
+ for i in $(CFILES); do cp src/$$i dist/$$i; done
+
+
+clean:
+ rm dist -rf
diff --git a/core/contrib/uci/luci b/core/contrib/uci/luci
new file mode 100644
index 000000000..bae2f48ce
--- /dev/null
+++ b/core/contrib/uci/luci
@@ -0,0 +1,35 @@
+config core main
+ option lang de
+ option mediaurlbase /ffluci/media
+
+config core category_privileges
+ option public nobody:nogroup
+
+config extern flash_keep
+ option uci "/etc/config"
+ option dropbear "/etc/dropbear"
+ option openvpn "/etc/openvpn"
+ option passwd "/etc/passwd"
+ option ipkg "/etc/ipkg.conf"
+ option httpd "/etc/httpd.conf"
+ option firewall "/etc/firewall.user"
+
+config public contact
+ option nickname
+ option name
+ option mail
+ option phone
+ option location
+ option geo
+ option note
+
+
+config event uci_oncommit
+ option network "/etc/init.d/network restart"
+ option wireless "/etc/init.d/network restart"
+ option olsrd "/etc/init.d/olsrd restart"
+ option dhcp "/etc/init.d/dnsmasq restart"
+ option luci_fw "/etc/init.d/luci_fw restart"
+ option dropbear "/etc/init.d/dropbear restart"
+ option httpd "/etc/init.d/httpd restart"
+ option fstab "/etc/init.d/fstab restart" \ No newline at end of file
diff --git a/core/ffluci b/core/ffluci
new file mode 100755
index 000000000..e090d560c
--- /dev/null
+++ b/core/ffluci
@@ -0,0 +1,5 @@
+#!/usr/bin/haserl --shell=luac
+package.path = "/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;" .. package.path
+package.cpath = "/usr/lib/lua/?.so;" .. package.cpath
+require("ffluci").dispatch()
+
diff --git a/core/ffluci-upload b/core/ffluci-upload
new file mode 100755
index 000000000..0128c2dd7
--- /dev/null
+++ b/core/ffluci-upload
@@ -0,0 +1,4 @@
+#!/usr/bin/haserl --shell=luac --upload-limit=6144
+-- This is a bit hacky: remove -upload from SCRIPT_NAME
+ENV.SCRIPT_NAME = ENV.SCRIPT_NAME:sub(1, #ENV.SCRIPT_NAME - 7)
+dofile("ffluci") \ No newline at end of file
diff --git a/core/index.cgi b/core/index.cgi
new file mode 100755
index 000000000..c9c98b0b2
--- /dev/null
+++ b/core/index.cgi
@@ -0,0 +1,3 @@
+#!/usr/bin/haserl --shell=luac
+print("Status: 302 Found")
+print("Location: ffluci/admin\n")
diff --git a/core/index.html b/core/index.html
new file mode 100644
index 000000000..58387a5fe
--- /dev/null
+++ b/core/index.html
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="refresh" content="0; URL=/cgi-bin/index.cgi" />
+</head>
+<body style="background-color: black">
+<a style="color: white; text-decoration: none" href="/cgi-bin/index.cgi">FFLuCI - Freifunk Lua Configuration Interface</a>
+</body>
+</html> \ No newline at end of file
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("&", "&amp;")
+ value = value:gsub('"', "&quot;")
+ value = value:gsub("'", "&apos;")
+ value = value:gsub("<", "&lt;")
+ return value:gsub(">", "&gt;")
+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