summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNatanael Copa <natanael.copa@gmail.com>2007-07-27 12:53:38 +0000
committerNatanael Copa <natanael.copa@gmail.com>2007-07-27 12:53:38 +0000
commitdc53423183a0c459284ebd139022b707f01af006 (patch)
tree8a67a2904ec991028bddd429d57eec114b05baab
parent275c80281ba2e84b8d810bdb1c2b7f8c9a4333d9 (diff)
downloadacf-core-dc53423183a0c459284ebd139022b707f01af006.tar.bz2
acf-core-dc53423183a0c459284ebd139022b707f01af006.tar.xz
moved core files to new dir structurev2.0_alpha1
git-svn-id: svn://svn.alpinelinux.org/acf/core/trunk@219 ab2d0c66-481e-0410-8bed-d214d4d58bed
-rw-r--r--ChangeLog29
-rw-r--r--Makefile53
-rw-r--r--README11
-rw-r--r--TODO8
-rw-r--r--app/Makefile49
-rw-r--r--app/README48
-rw-r--r--app/acf-util/logon-controller.lua60
-rw-r--r--app/acf-util/logon-html.lsp19
-rw-r--r--app/acf-util/logon-model.lua61
-rw-r--r--app/acf_www-controller.lua202
-rw-r--r--app/dummy/bar-controller.lua35
-rw-r--r--app/dummy/bar-html.lsp3
-rw-r--r--app/dummy/bar.menu3
-rw-r--r--app/foo-controller.lua36
-rw-r--r--app/foo-html.lsp3
-rw-r--r--app/foo.menu2
-rw-r--r--app/menuhints.menu8
-rw-r--r--app/template-html.lsp99
-rw-r--r--app/welcome-controller.lua27
-rw-r--r--app/welcome-html.lsp3
-rw-r--r--app/welcome.menu1
-rw-r--r--config.mk10
-rw-r--r--lib/Makefile47
-rw-r--r--lib/README3
-rw-r--r--lib/ed.lua60
-rw-r--r--lib/fs.lua89
-rw-r--r--lib/html.lua245
-rw-r--r--lib/join.lua25
-rw-r--r--lib/log_view.lua56
-rw-r--r--lib/menubuilder.lua150
-rw-r--r--lib/service_controller.lua189
-rw-r--r--lib/service_model.lua115
-rw-r--r--lib/session.lua143
-rw-r--r--lib/split.lua31
-rwxr-xr-xlib/validator.lua156
-rw-r--r--lib/web_elements.lua156
-rw-r--r--www/Makefile50
-rwxr-xr-xwww/cgi-bin/acf19
-rwxr-xr-xwww/cgi-bin/mvc.lua278
-rw-r--r--www/index.html12
-rw-r--r--www/sample.html102
-rw-r--r--www/static/alpine.gifbin0 -> 17876 bytes
-rw-r--r--www/static/alpine.pngbin0 -> 43938 bytes
-rw-r--r--www/static/arctic-back.pngbin0 -> 213 bytes
-rw-r--r--www/static/arctic-upleft.pngbin0 -> 300 bytes
-rw-r--r--www/static/greypx.gifbin0 -> 38 bytes
-rw-r--r--www/static/selected.gifbin0 -> 1095 bytes
-rw-r--r--www/static/submenu.css52
-rw-r--r--www/static/unselected.gifbin0 -> 945 bytes
-rw-r--r--www/static/webconf-purple.css313
-rw-r--r--www/static/webconf.css367
51 files changed, 3428 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..87b06b7
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,29 @@
+2006-09-25
+ 0.1.3
+ Renamed to acf (Alpine Configuration Framework), made new config directories
+ wc table renamed to cf (wc means watercloset, and although this code stinks,
+ it doesn't stink THAT bad)
+2006-09-20
+ 0.1.2
+ The lua portions are integrated now. Other developers should be able to build
+ code from c/hostname.lua and m/hostname.lua
+2006-09-14
+ 0.1.1
+ Clearsilver removed, haserll is used - Webconf on LUA!
+2006-09-03
+ 0.1.0
+ Another attempt at simplifying... this one uses clearsilver.
+2006-08-22
+ 0.0.7
+ Moved the preabmle/postamble stuff into an array that's executed, so that each
+ view doesn't have to do it. (DRY)
+
+ Changed css to do dt/dd instead of label; This makes error messages for fields easier to
+ tag.
+ 0.0.5
+ Added DESTDIR to the APPROOT root (N.Copa requested)
+2006-08-21
+ 0.0.3 version
+ create APPROOT and WEBROOT targets; moved www/app to www/cgi-bin (works with bb httpd)
+2006-08-15
+ 0.0.1 version released - webconf now has a makefile
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..73e35f4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,53 @@
+APP_NAME=core
+PACKAGE=acf-$(APP_NAME)
+VERSION=2.0_alpha1
+
+P=$(PACKAGE)-$(VERSION)
+DISTDIR:=$(PWD)/$(P)
+DISTPKG=$(P).tar.bz2
+
+SUBDIRS=app lib www
+EXTRA_DIST=ChangeLog Makefile README TODO
+
+DISTFILES=$(EXTRA_DIST)
+
+CP=cp
+TAR=tar
+
+RECURSIVE_TARGETS=all-recursive install-recursive distdir-recursive \
+ clean-recursive
+phony+=$(RECURSIVE_TARGETS)
+
+export DISTDIR DESTDIR
+$(RECURSIVE_TARGETS):
+ target=`echo $@ | sed 's/-recursive//'`;\
+ for dir in $(SUBDIRS); do\
+ ( cd $$dir && $(MAKE) $$target ) || exit 1;\
+ done
+
+phony += all
+all: all-recursive
+
+phony += clean
+clean: clean-recursive
+ rm -rf $(DISTDIR) $(DISTPKG)
+
+phony += distdir
+distdir: distdir-recursive $(DISTFILES)
+ for i in $(DISTFILES) ; do\
+ dest="$(DISTDIR)/$$i";\
+ mkdir -p `dirname $$dest` &&\
+ $(CP) "$$i" "$$dest" || exit 1;\
+ done
+
+phony += dist
+dist: $(DISTPKG)
+
+$(DISTPKG): distdir $(DISTFILES)
+ $(TAR) -chjf $@ $(P)
+ rm -r $(DISTDIR)
+
+phony+=install
+install: install-recursive
+
+.PHONY: $(phony)
diff --git a/README b/README
new file mode 100644
index 0000000..b5d8df5
--- /dev/null
+++ b/README
@@ -0,0 +1,11 @@
+Alpine Configuration Framework
+(A work in progress.)
+
+You can checout from svn to another location and run "make checkpkg DESTDIR=/"
+to make sure that all files get included in dist package.
+
+The config file should be in /etc/acf/acf.conf and contain:
+
+appdir=/var/lib/acf/app/
+
+(or wherever your app dir is)
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..0162583
--- /dev/null
+++ b/TODO
@@ -0,0 +1,8 @@
+Todo items
+
+* Menu controller
+* Role Based Access Control
+* Clean up the code - is m/c/v as simple as it can be?
+* Work on a non-trivial webconf (openssl CA or interfaces file) to see how hard this is going to be
+* Work at a slightly faster than glacial pace
+* fix Makefiles so ncopa can create packages again
diff --git a/app/Makefile b/app/Makefile
new file mode 100644
index 0000000..4c12bea
--- /dev/null
+++ b/app/Makefile
@@ -0,0 +1,49 @@
+include ../config.mk
+
+APP_DIST= acf-util/logon-controller.lua\
+ acf-util/logon-html.lsp\
+ acf-util/logon-model.lua\
+ dummy/bar-controller.lua\
+ dummy/bar-html.lsp\
+ dummy/bar.menu\
+ acf_www-controller.lua\
+ foo-controller.lua\
+ foo-html.lsp\
+ foo.menu\
+ menuhints.menu\
+ template-html.lsp\
+ welcome-controller.lua\
+ welcome-html.lsp\
+ welcome.menu\
+
+EXTRA_DIST=README Makefile
+DISTFILES=$(APP_DIST) $(EXTRA_DIST)
+
+install_dir=$(DESTDIR)/$(appdir)
+
+phony+=all
+all:
+
+phony+=clean
+clean:
+
+phony+=distdir
+distdir: $(DISTFILES)
+ mkdir -p "$(DISTDIR)/app"
+ for i in $(DISTFILES); do\
+ dest=`dirname "$(DISTDIR)/app/$$i"`;\
+ mkdir -p "$$dest";\
+ cp "$$i" "$$dest";\
+ done
+
+phony+=install
+install:
+ mkdir -p $(install_dir)
+ for i in $(APP_DIST); do\
+ dest=`dirname "$(install_dir)/$$i"`;\
+ mkdir -p "$$dest";\
+ cp "$$i" "$$dest";\
+ done
+
+
+.PHONY: $(phony)
diff --git a/app/README b/app/README
new file mode 100644
index 0000000..83e69af
--- /dev/null
+++ b/app/README
@@ -0,0 +1,48 @@
+Each directory has a collection of controllers, models and views
+for a specific package.
+
+
+How to make a new package
+-------------------------
+
+Create a directory for your package with the package name.
+In this dir you put your controller, model, menu files.
+
+Create a Makefile that describes the the following:
+
+ * package name
+
+ * version of package
+
+ * what files should be installed on the running system
+
+ * what additional files should be included in the distribution package
+ (tar.gz source package). The Makefile and acf-pkg.mk wil automatically be
+ added so only extra files like README and ChangeLog needs to be specified
+ here.
+
+ * just include the acf-pkg.mk makefile which contains the rules to install
+ the package and build the dist package-
+
+If you have an acf-foo package it should look like this:
+
+PACAKGE = foo
+VERSION = 0.2.0
+INSTALL_FILES = foo.controller.lua foo.model.lua
+EXTRA_DIST = ChangeLog README
+include acf-pkg.mk
+
+
+Then you just need to create a link to ../acf-pkg.mk
+
+ ln -s ../acf-pkg.mk acf-pkg.mk
+
+To build the distribution package you then run:
+
+ make dist
+
+To install the files in the running system:
+
+ make install
+
+
diff --git a/app/acf-util/logon-controller.lua b/app/acf-util/logon-controller.lua
new file mode 100644
index 0000000..8359c18
--- /dev/null
+++ b/app/acf-util/logon-controller.lua
@@ -0,0 +1,60 @@
+-- Logon / Logoff functions
+
+module (..., package.seeall)
+
+require ("session")
+
+mvc.on_load = function(self, parent)
+ -- If they specify an invalid action or try to run init, then redirect
+ -- to the read function.
+ if ( self.conf.action == nil or self.conf.action == "init" ) then
+ -- do what?
+ end
+
+end
+
+
+logon = function(self)
+
+ local username=cfe({ name="username" })
+ local password=cfe({ name="password" })
+ local logon=cfe({ name="Logon", type="submit"})
+ local s = ""
+
+ if self.clientdata.username and self.clientdata.password then
+ if self.model.logon(self, self.clientdata.username, self.clientdata.password) == false then
+ username.value = self.clientdata.username
+ if self.session.id then
+ username.errtxt = "You are already logged in. Logout first."
+ else
+ username.errtxt = "There was a problem logging in"
+ end
+ else
+ self.conf.controller = ""
+ self.conf.action = ""
+ self.conf.prefix = ""
+ self.conf.type = "redir"
+ error(self.conf)
+ end
+ end
+ -- If we reach this point, just give them the login page
+ return ( cfe ({type="form",
+ option={ script=ENV["SCRIPT_NAME"],
+ prefix=self.conf.prefix,
+ controller = self.conf.controller,
+ action = "logon" },
+ value = { username, password, logon } }))
+end
+
+
+logout = function(self)
+ self.model.logout(self, session.id)
+
+
+ -- and raise an error to go to the homepage
+ self.conf.action = ""
+ self.conf.prefix = ""
+ self.conf.controller = ""
+ self.conf.type = "redir"
+ error(self.conf)
+end
diff --git a/app/acf-util/logon-html.lsp b/app/acf-util/logon-html.lsp
new file mode 100644
index 0000000..cdac2bf
--- /dev/null
+++ b/app/acf-util/logon-html.lsp
@@ -0,0 +1,19 @@
+<? local form = ... ?>
+<h1>Logon</h1>
+
+<form action="<?= form.option.script .. form.option.prefix ..
+ form.option.controller .. "/" .. form.option.action ?>" method="POST">
+<table>
+<? local myform = form.value
+ for k,v in pairs(myform) do ?>
+<tr><td><?= v.name ?></td><td>
+<? if v.type == "submit" then ?>
+ <input type="submit" name="<?= v.name ?>" value="Logon">
+<? else ?>
+ <input type="text" name="<?= v.name ?>">
+ <font color=red><?= v.errtxt ?></font>
+<? end ?>
+</td></tr>
+<? end ?>
+</table>
+</form>
diff --git a/app/acf-util/logon-model.lua b/app/acf-util/logon-model.lua
new file mode 100644
index 0000000..dbd8522
--- /dev/null
+++ b/app/acf-util/logon-model.lua
@@ -0,0 +1,61 @@
+-- Logon / Logoff model functions
+
+module (..., package.seeall)
+
+local sess = require ("session")
+
+local pvt = {}
+
+
+-- return a sessionid if username / password is valid, false
+-- /etc/acf/passwd should be lines of userid:passwd:user name:role1[,role2[,role3]]
+pvt.logon = function (self, id, passwd )
+ -- if we already have sessionid... then you are already logged in
+ if self.session.id then
+ return false
+ end
+
+ id = id or ""
+ passwd = passwd or ""
+
+ -- open our hokey password file,
+ local f = io.open(self.conf.confdir .. "/passwd" )
+ if f then
+ m = f:read("*all") .. "\n"
+ f:close()
+
+ for l in string.gmatch(m, "(%C*)\n") do
+ local userid, password, username, roles =
+ string.match(l, "([^:]*):([^:]*):([^:]*):(.*)")
+ if userid == id and password == passwd then
+ self.session.id = sess.random_hash(512)
+ self.session.name = username
+ self.session.roles = roles
+ break
+ end
+ end
+ end
+ if self.session.id then
+ local x = require("session")
+ x.save_session(self.conf.sessiondir, self.session.id, self.session)
+ x=nil
+ return self.session.id
+ else
+ return false
+ end
+end
+
+-- invalidate the session, or return false if the session wasn't valid
+pvt.logout = function (self, sessionid)
+
+ sess.invalidate_session ( self.conf.sessiondir, sessionid)
+ self.session = {}
+
+end
+
+-------------------------------------------------------------------------
+-- Public Methods
+-------------------------------------------------------------------------
+
+logon = pvt.logon
+logout = pvt.logout
diff --git a/app/acf_www-controller.lua b/app/acf_www-controller.lua
new file mode 100644
index 0000000..742533a
--- /dev/null
+++ b/app/acf_www-controller.lua
@@ -0,0 +1,202 @@
+--[[ Code for the Alpine Configuration WEB framework
+ Written for Alpine Configuration Framework (ACF) -- see www.alpinelinux.org
+ Copyright (C) 2007 Nathan Angelacos
+ Licensed under the terms of GPL2
+ ]]--
+module(..., package.seeall)
+
+-- We use the parent exception handler in a last-case situation
+local parent_exception_handler
+
+mvc = {}
+mvc.on_load = function (self, parent)
+
+ -- Make sure we have some kind of sane defaults for libdir and sessiondir
+ self.conf.libdir = self.conf.libdir or ( self.conf.appdir .. "/lib/" )
+ self.conf.sessiondir = self.conf.sessiondir or "/tmp/"
+ self.conf.appuri = "http://" .. ENV.HTTP_HOST .. ENV.SCRIPT_NAME
+ self.conf.default_controller = "welcome"
+ self.clientdata = FORM
+
+
+ parent_exception_handler = parent.exception_handler
+
+ -- this sets the package path for us and our children
+ package.path= self.conf.libdir .. "?.lua;" .. package.path
+
+ self.session = {}
+ local x=require("session")
+ if FORM.sessionid then
+ local timestamp
+ timestamp , self.session = x.load_session(self.conf.sessiondir , FORM.sessionid)
+ self.session.id = FORM.sessionid
+ else
+ self.session.id = nil
+ end
+ logit ("acf_www-controller mvc.on_load activated" )
+end
+
+mvc.pre_exec = function ()
+ logit ("acf_www-controller mvc.pre_exec activated")
+end
+
+mvc.post_exec = function ()
+ logit ("acf_www-controller mvc.post_exec activated")
+end
+
+
+-- look for a template
+-- ctlr-action-view, then ctlr-view, then action-view, then view
+
+find_template = function ( appdir, prefix, controller, action, viewtype )
+ local targets = {
+ appdir .. prefix .. "template-" .. controller .. "-" ..
+ action .. "-" .. viewtype .. ".lsp",
+ appdir .. prefix .. "template-" .. controller .. "-" ..
+ viewtype .. ".lsp",
+ appdir .. prefix .. "template-" .. action .. "-" ..
+ viewtype .. ".lsp",
+ appdir .. prefix .. "template-" .. viewtype .. ".lsp"
+ }
+ local file
+ for k,v in pairs(targets) do
+ file = io.open (v)
+ if file then
+ io.close (file)
+ return v
+ end
+ end
+ -- not found, so try one level higher
+ if prefix == "" then -- already at the top level - fail
+ return false
+ end
+ prefix = dirname (prefix)
+ return find_template ( appdir, prefix, controller, action, viewtype )
+end
+
+
+
+-- Overload the MVC's view resolver with our own
+
+view_resolver = function(self)
+ local file
+ local viewname
+ local viewtype = self.conf.viewtype or "html"
+ local names = { self.conf.appdir .. self.conf.prefix .. self.conf.controller ..
+ "-" .. self.conf.action .. "-" .. viewtype .. ".lsp",
+ self.conf.appdir .. self.conf.prefix .. self.conf.controller ..
+ "-" .. viewtype .. ".lsp" }
+
+
+ -- FIXME: MVC doesn't have a way to call a function after the controller is run
+ -- so we serialize the session in the view resolver. MVC probably should have
+ -- a postinit or postrun method...
+ if self.session.id then
+ local x = require("session")
+ x.save_session(self.conf.sessiondir, self.session.id, self.session)
+ x=nil
+ end
+
+ -- search for template
+ local template = find_template ( self.conf.appdir, self.conf.prefix,
+ self.conf.controller, self.conf.action, "html")
+
+ -- search for view
+ for i,filename in ipairs (names) do
+ file = io.open(filename)
+ if file then
+ file:close()
+ viewname = filename
+ break
+ end
+ end
+
+ -- We have a template
+ if template then
+ -- ***************************************************
+ -- This is how to call another controller (APP or self
+ -- can be used... m will contain worker and model,
+ -- with conf, and other "missing" parts pointing back
+ -- to APP or self
+ -- ***************************************************
+ local m = self:new("alpine-baselayout/hostname")
+ local h = m.worker.read(m)
+
+ local pageinfo = { viewfile = viewname,
+ controller = m.conf.controller,
+ -- ^^^ see.. m.conf doesnt exist - but it works
+ action = self.conf.action,
+ hostname = h.hostname.value,
+ prefix = self.conf.prefix,
+ script = self.conf.appuri
+ }
+
+ -- Build the menu table
+ m=require("menubuilder")
+ local menu = m.get_menuitems(self.conf.appdir)
+
+ -- A quick hack
+ submenu = {}
+ for k,v in pairs ( self.worker ) do
+ if type ( self.worker[k] ) == "function" then
+ table.insert (submenu, k)
+ end
+ end
+
+ return function (viewtable)
+ local template = haserl.loadfile (template)
+ return template ( pageinfo, menu, submenu, viewtable, self.session )
+ end
+ end
+
+ -- No template, but have a view
+ if viewname then
+ return haserl.loadfile (viewname)
+ else
+ return function() end
+ end
+end
+
+exception_handler = function (self, message )
+ local html = require ("html")
+ if type(message) == "table" then
+ if message.type == "redir" then
+ io.write ("Status: 302 Moved\n")
+ io.write ("Location: " .. ENV["SCRIPT_NAME"] ..
+ message.prefix .. message.controller ..
+ "/" .. message.action ..
+ (message.extra or "" ) .. "\n")
+ if self.session.id then
+ io.write (html.cookie.set("sessionid", self.session.id))
+ else
+ io.write (html.cookie.unset("sessionid"))
+ end
+ io.write ( "Content-Type: text/html\n\n" )
+ elseif message.type == "dispatch" then
+ parent_exception_handler(self, message)
+ end
+ else
+ parent_exception_handler( self, message)
+ end
+end
+
+-- create a Configuration Framework Entity (cfe)
+-- returns a table with at least "value", "type", "option" and "errtxt"
+cfe = function ( optiontable )
+ optiontable = optiontable or {}
+ me = { value="",
+ type="text",
+ option="",
+ errtxt="",
+ name="" }
+ for key,value in pairs(optiontable) do
+ me[key] = value
+ end
+ return me
+end
+
+
+-- syslog something
+logit = function ( ... )
+ os.execute ( "logger \"" .. ... .. "\"" )
+end
diff --git a/app/dummy/bar-controller.lua b/app/dummy/bar-controller.lua
new file mode 100644
index 0000000..e0b9999
--- /dev/null
+++ b/app/dummy/bar-controller.lua
@@ -0,0 +1,35 @@
+-- A standin controller for testing
+
+module (..., package.seeall)
+
+-- Cause an http redirect to our "read" action
+-- We use the self.conf table because it already has prefix,controller,etc
+-- The redir code is defined in the application error handler (acf-controller)
+local list_redir = function (self)
+ self.conf.action = "read"
+ self.conf.type = "redir"
+ error (self.conf)
+end
+
+
+--init ( init by definition is a public method)
+init = function(self, parent)
+ -- If they specify an invalid action or try to run init, then redirect
+ -- to the read function.
+ if ( self.conf.action == nil or self.conf.action == "init" ) then
+ list_redir(self)
+ end
+ -- Make a function by the action name
+ self.worker[self.conf.action] = function ()
+ return cfe ( {name=self.conf.action, value=self.conf.action} )
+
+ end
+
+end
+
+
+read = function (self )
+ return ( { } )
+end
+
+
diff --git a/app/dummy/bar-html.lsp b/app/dummy/bar-html.lsp
new file mode 100644
index 0000000..708e1ad
--- /dev/null
+++ b/app/dummy/bar-html.lsp
@@ -0,0 +1,3 @@
+<? view = ... ?>
+<h1>Action <?= view.name ?></h1>
+<p>This is a null controller for menu and authorization testing.</p>
diff --git a/app/dummy/bar.menu b/app/dummy/bar.menu
new file mode 100644
index 0000000..1fb42b8
--- /dev/null
+++ b/app/dummy/bar.menu
@@ -0,0 +1,3 @@
+# Prefix and controller are already known at this point
+# Cat Group Tab Action
+Test Bar_controller bar read
diff --git a/app/foo-controller.lua b/app/foo-controller.lua
new file mode 100644
index 0000000..f25cbaf
--- /dev/null
+++ b/app/foo-controller.lua
@@ -0,0 +1,36 @@
+-- A standin controller for testing
+
+module (..., package.seeall)
+
+x = require ("session")
+
+-- Cause an http redirect to our "read" action
+-- We use the self.conf table because it already has prefix,controller,etc
+-- The redir code is defined in the application error handler (acf-controller)
+local list_redir = function (self)
+ self.conf.action = "read"
+ self.conf.type = "redir"
+ error (self.conf)
+end
+
+
+mvc.on_load = function(self, parent)
+ -- If they specify an invalid action or try to run init, then redirect
+ -- to the read function.
+ if ( self.conf.action == nil or self.conf.action == "init" ) then
+ list_redir(self)
+ end
+ -- Make a function by the action name
+ self.worker[self.conf.action] = function ()
+ return cfe ( {name=self.conf.action, value=self.conf.action} )
+
+ end
+
+end
+
+
+read = function (self )
+ return ( { } )
+end
+
+
diff --git a/app/foo-html.lsp b/app/foo-html.lsp
new file mode 100644
index 0000000..708e1ad
--- /dev/null
+++ b/app/foo-html.lsp
@@ -0,0 +1,3 @@
+<? view = ... ?>
+<h1>Action <?= view.name ?></h1>
+<p>This is a null controller for menu and authorization testing.</p>
diff --git a/app/foo.menu b/app/foo.menu
new file mode 100644
index 0000000..e9244f0
--- /dev/null
+++ b/app/foo.menu
@@ -0,0 +1,2 @@
+#cat Group tab action
+Test foo_controller foo read
diff --git a/app/menuhints.menu b/app/menuhints.menu
new file mode 100644
index 0000000..5d36abb
--- /dev/null
+++ b/app/menuhints.menu
@@ -0,0 +1,8 @@
+# This gives basic priorities to the groups in the menu
+# That way, individual menu files don't have to say "80Setup" everywhere
+# Note, however, that if ANY individual menu sets Setup to less than 80, then
+# That number will override the defaults set here.
+# (i.e. Lowest specifically set priority wins)
+10Home
+80Setup
+100Test
diff --git a/app/template-html.lsp b/app/template-html.lsp
new file mode 100644
index 0000000..784bb4e
--- /dev/null
+++ b/app/template-html.lsp
@@ -0,0 +1,99 @@
+<? local pageinfo , mainmenu, submenu, viewtable, session = ...
+ html=require("html") ?>
+Status: 200 OK
+Content-Type: text/html
+<? if (session.id) then
+ io.write( html.cookie.set("sessionid", session.id) )
+ else
+ io.write (html.cookie.unset("sessionid"))
+ end
+?>
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DDD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+<title><?= pageinfo.hostname .. " - " .. pageinfo.controller .. "->" .. pageinfo.action ?></title>
+<link rel="stylesheet" type="text/css" href="/static/webconf.css">
+</head>
+<body>
+
+<div id=head>
+<h1>Alpine</h1>
+<p>Host: <em><?= pageinfo.hostname ?></em></p>
+<? -- a quick hack
+ local ctlr = pageinfo.script .. "/acf-util/logon/"
+?>
+<? if session.id == nil then
+ io.write ( html.link( { label = "Log in", value = ctlr .. "logon" } ) )
+ else
+ io.write (html.link( { label = "Logout as " .. ( session.name or "unkown") , value = ctlr .. "logout" } ) )
+ end ?></p>
+</div>
+
+<div id="mainmenu">
+<?
+ -- FIXME: This needs to go in a library function somewhere (menubuilder?)
+ local cat, group
+ local liston=false
+ local selected
+ for k,v in ipairs(mainmenu) do
+ if v.cat ~= cat then
+ if liston == true then
+ io.write ("</ul>\n")
+ liston=false
+ end
+ cat = v.cat
+ io.write (string.format("<h3>%s</h3>\n", cat))
+ group = ""
+ end
+ if v.group ~= group then
+ group = v.group
+ if liston == false then
+ io.write ("<ul>")
+ liston=true
+ end
+ if pageinfo.prefix == v.prefix .. "/" and
+ pageinfo.controller == v.controller then
+ selected=" id=\"selected\""
+ else
+ selected=""
+ end
+ io.write (string.format("<li%s><a href=\"%s%s/%s/%s\">%s</a></li>\n",
+ selected, ENV.SCRIPT_NAME,v.prefix, v.controller, v.action, v.group))
+ end
+ end
+?>
+</ul>
+</div>
+
+
+<div id="submenu">
+<h2><?= pageinfo.prefix ?> > <?= pageinfo.controller .. " > " .. pageinfo.action ?></h2>
+<ul>
+<? for k,v in pairs(submenu) do
+ if v == pageinfo.action then
+ io.write (string.format('<li id="selected">%s</li>\n',
+ v, v ))
+ else
+ io.write (string.format('<li><a href="%s">%s</a></li>\n',
+ v, v ))
+ end
+ end
+?>
+</ul>
+</div>
+
+
+
+<div id="content">
+<? local func = haserl.loadfile(pageinfo.viewfile)
+ func (viewtable) ?>
+</div>
+
+
+<div id="footer">
+<p><center>Made with care by acf</center></p>
+</div>
+</body>
+</html>
diff --git a/app/welcome-controller.lua b/app/welcome-controller.lua
new file mode 100644
index 0000000..450952c
--- /dev/null
+++ b/app/welcome-controller.lua
@@ -0,0 +1,27 @@
+-- A standin controller for testing
+
+module (..., package.seeall)
+
+-- Cause an http redirect to our "read" action
+-- We use the self.conf table because it already has prefix,controller,etc
+-- The redir code is defined in the application error handler (acf-controller)
+local list_redir = function (self)
+ self.conf.action = "read"
+ self.conf.type = "redir"
+ error (self.conf)
+end
+
+mvc = {}
+mvc.on_load = function(self, parent)
+ -- It doesn't matter what action they choose - we only support read
+ if ( self.conf.action ~= "read") then
+ list_redir(self)
+ end
+end
+
+
+read = function (self )
+ return ( { } )
+end
+
+
diff --git a/app/welcome-html.lsp b/app/welcome-html.lsp
new file mode 100644
index 0000000..7f205fe
--- /dev/null
+++ b/app/welcome-html.lsp
@@ -0,0 +1,3 @@
+<? view = ... ?>
+<h1>Alpine Configuration Framework</h1>
+<p>Welcome.</p>
diff --git a/app/welcome.menu b/app/welcome.menu
new file mode 100644
index 0000000..51c25f1
--- /dev/null
+++ b/app/welcome.menu
@@ -0,0 +1 @@
+10Home Welcome Welcome Read
diff --git a/config.mk b/config.mk
new file mode 100644
index 0000000..40c9722
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,10 @@
+prefix=/usr
+datadir=${prefix}/share
+sysconfdir=${prefix}/etc
+localstatedir=${prefix}/var
+acfdir=${datadir}/acf
+wwwdir=${acfdir}/www
+cgibindir=${wwwdir}/cgi-bin
+appdir=${acfdir}/app
+acflibdir=${acfdir}/lib
+sessionsdir=${localstatedir}/lib/acf/sessions
diff --git a/lib/Makefile b/lib/Makefile
new file mode 100644
index 0000000..0c65ca1
--- /dev/null
+++ b/lib/Makefile
@@ -0,0 +1,47 @@
+include ../config.mk
+
+LIB_DIST=ed.lua\
+ fs.lua\
+ html.lua\
+ join.lua\
+ log_view.lua\
+ menubuilder.lua\
+ service_controller.lua\
+ service_model.lua\
+ session.lua\
+ split.lua\
+ validator.lua\
+ web_elements.lua
+
+EXTRA_DIST=README Makefile
+DISTFILES=$(LIB_DIST) $(EXTRA_DIST)
+
+install_dir=$(DESTDIR)/$(acflibdir)
+dist_dir=$(DISTDIR)/$(notdir $(PWD))
+
+phony+=all
+all:
+
+phony+=clean
+clean:
+
+phony+=distdir
+distdir: $(DISTFILES)
+ mkdir -p "$(dist_dir)"
+ for i in $(DISTFILES); do\
+ dest=`dirname "$(dist_dir)/$$i"`;\
+ mkdir -p "$$dest";\
+ cp "$$i" "$$dest";\
+ done
+
+phony+=install
+install:
+ mkdir -p $(install_dir)
+ for i in $(LIB_DIST); do\
+ dest=`dirname "$(install_dir)/$$i"`;\
+ mkdir -p "$$dest";\
+ cp "$$i" "$$dest";\
+ done
+
+
+.PHONY: $(phony)
diff --git a/lib/README b/lib/README
new file mode 100644
index 0000000..cbc9798
--- /dev/null
+++ b/lib/README
@@ -0,0 +1,3 @@
+In the process of cleaning up...
+shell libs are no longer used, just keeping them for historical reference at this point
+lua libs are being worked on.
diff --git a/lib/ed.lua b/lib/ed.lua
new file mode 100644
index 0000000..2e86f08
--- /dev/null
+++ b/lib/ed.lua
@@ -0,0 +1,60 @@
+#!/usr/bin/lua
+
+require "object"
+
+-- ed object
+Ed = Object:new{
+ filename = nil,
+ lines = {}
+}
+
+
+-- openfile and read it to table
+function Ed:open( filename, mode )
+ local f = io.open( filename, mode )
+ -- check that open was success
+ if f == nil then
+ return nil
+ end
+
+ -- read the lines
+ for line in f:lines() do
+ table.insert( self.lines, line )
+ end
+ f:close()
+ self.filename = filename
+ return self.lines
+end
+
+
+-- search and replace on lines that matches linematch
+function Ed:find_gsub( linematch, search, replace, limit )
+ local i, line
+ for i, line in ipairs( self.lines ) do
+ if string.find( line, linematch ) then
+ self.lines[i] = string.gsub( line, search, replace, limit )
+ end
+ end
+end
+
+
+-- Write the table to file again
+function Ed:flush( filename )
+ local f = io.open( filename, "w" )
+ if f == nil then
+ return false
+ end
+
+ -- write each line to file
+ for i, line in ipairs( self.lines ) do
+ f:write(line .. "\n") -- test this!
+ end
+ f:close()
+ return true
+end
+
+function Ed:insert( line )
+ self.lines:insert( line )
+end
+
+
diff --git a/lib/fs.lua b/lib/fs.lua
new file mode 100644
index 0000000..d171074
--- /dev/null
+++ b/lib/fs.lua
@@ -0,0 +1,89 @@
+--[[
+ module for generic filesystem funcs
+
+ Copyright (c) Natanael Copa 2006
+
+]]--
+
+module (..., package.seeall)
+
+require ("lfs")
+
+-- generic wrapper funcs
+function is_dir ( pathstr )
+ return lfs.attributes ( pathstr, "mode" ) == "directory"
+end
+
+function is_file ( pathstr )
+ return lfs.attributes ( pathstr, "mode" ) == "file"
+end
+
+-- Returns the contents of a file as a string
+function read_file ( path )
+ local file = io.open(path)
+ if ( file ) then
+ local f = file:read("*a")
+ file:close()
+ return f
+ else
+ return nil
+ end
+end
+
+-- Returns an array with the contents of a file,
+-- or nil and the error message
+function read_file_as_array ( path )
+ local file, error = io.open(path)
+ if ( file == nil ) then
+ return nil, error
+ end
+ local f = {}
+ for line in file:lines() do
+ table.insert ( f , line )
+ end
+ file:close()
+ return f
+end
+
+
+
+
+-- write a string to a file
+function write_file ( path, str )
+ local file = io.open(path, "w")
+ if ( file ) then
+ file:write(str)
+ file:close()
+ end
+end
+
+
+-- iterator function for finding dir entries matching filespec (what)
+-- starting at where, or currentdir if not specified.
+-- Finds regexes, not fileglobs
+function find ( what, where )
+ -- returns an array of files under "where" that match what "f"
+ local function find_files_as_array ( f, where, t )
+ where = where or lfs.currentdir()
+ f = f or ".*"
+ t = t or {}
+ for d in lfs.dir ( where ) do
+ if fs.is_dir ( where .. "/" .. d ) and (d ~= ".") and ( d ~= "..") then
+ find_files_as_array (f, where .. "/" .. d, t )
+ end
+ if (string.match (d, "^" .. f .. "$" )) then
+ table.insert (t, ( string.gsub ( where .. "/" .. d, "/+", "/" ) ) )
+ end
+ end
+ return (t)
+ end
+
+ -- This is the iterator
+ local t = find_files_as_array ( what, where )
+ local idx = 0
+ return function ()
+ idx = idx + 1
+ return t[idx]
+ end
+end
+
diff --git a/lib/html.lua b/lib/html.lua
new file mode 100644
index 0000000..7e60c2c
--- /dev/null
+++ b/lib/html.lua
@@ -0,0 +1,245 @@
+--[[ lowlevel html functions
+ Written for Alpine Configuration Framework (ACF) -- see www.alpinelinux.org
+ Copyright (C) 2007 Nathan Angelacos
+ Licensed under the terms of GPL2
+]]--
+module (..., package.seeall)
+
+--[[ Cookie functions ]]------------------------------------------------------
+cookie={}
+
+-- Set a cookie - returns a string suitable for setting a cookie
+-- if the value is the boolean "false", then set the cookie to expire
+cookie.set = function ( name, value, path )
+ local expires = ""
+ if name == nil then
+ return ("")
+ end
+ if value == false then
+ expires = 'expires=Thu Jan 1 00:00:00 EST 1970'
+ value = ""
+ end
+ if path == nil then
+ path = "/"
+ end
+ return (string.format('Set-Cookie: %s=%s; path=%s; %s\n', tostring(name),
+ tostring(value), path, expires))
+end
+
+
+-- wrapper function to clear a cookie
+cookie.unset = function ( name, path)
+ return cookie.set (name, false, path)
+end
+
+
+
+-- escape unsafe html characters
+function html_escape (text )
+ text = text or ""
+ local str = string.gsub (text, "&", "&amp;" )
+ str = string.gsub (str, "<", "&lt;" )
+ return string.gsub (str, ">", "&gt;" )
+end
+
+-- return a name,value pair as a string.
+nv_pair = function ( name, value)
+ if ( name == nil ) then
+ return ( value or "" )
+ end
+
+ if ( type(value) == "boolean" ) then
+ value = tostring(value)
+ end
+
+ if ( value == nil ) then
+ return ( "" )
+ else
+ return (string.format (' %s="%s" ', name , ( value or "" ) ))
+ end
+end
+
+
+--[[
+ each of these functions take a table that has an associative array of
+ the values we might care about:
+
+ value -- this is the value in the form element, or the selected element
+ name -- this is the name of the element
+ cols, rows
+ class
+ id
+ etc.
+]]--
+
+local generic_input = function ( field_type, v )
+ if type(v.value) == "table" then
+ ret = {}
+ local vals = v.value
+ for n, val in ipairs(vals) do
+ v.value = val
+ table.insert(ret, generic_input(field_type, v))
+ end
+ v.value = vals
+ return table.concat(ret)
+ end
+ if ( field_type == nil ) then
+ return nil
+ end
+
+ local str = string.format ( '<input type="%s" ', field_type )
+
+ for i,k in ipairs ( {
+ "name", "size", "checked", "maxlength",
+ "value", "length", "class", "id", "src",
+ "align", "alt",
+ "tabindex", "accesskey", "onfocus", "onblur"
+ } ) do
+ str = str .. nv_pair ( k, v[k] )
+ end
+
+ if ( v.disabled ~= nil ) then
+ str = str .. " disabled"
+ end
+
+ return ( str .. ">" )
+end
+
+
+--[[ Form functions ]]------------------------------------------------------
+-- These expect something like a cfe to work (see acf_www-controller.lua)
+
+form = {}
+form.text = function ( v )
+ return generic_input ( "text", v )
+end
+
+
+form.longtext = function ( v )
+ local str = "<textarea"
+ for i,k in ipairs ( {
+ "name", "rows", "cols",
+ "class", "id", "tabindex", "accesskey",
+ "onfocus", "onblur"
+ } ) do
+ str = str .. nv_pair ( k, v[k] )
+ end
+ str = str .. nv_pair (nil, v.disabled)
+ return ( str .. ">" .. (v.value or "" ) .. "</textarea>" )
+end
+
+
+function form.passwd ( v )
+ return generic_input ( "password", v )
+end
+
+function form.hidden ( v )
+ return generic_input ( "hidden", v )
+end
+
+
+function form.submit ( v )
+ return generic_input ( "submit", v )
+end
+
+
+function form.action (v)
+ return generic_input ("submit", v)
+end
+
+function form.file ( v )
+ return generic_input ( "file", v )
+end
+
+function form.image ( v )
+ return generic_input ( "image", v )
+end
+
+
+-- v.value is the selected item
+-- v.option is an array of valid options
+-- NOTE use of value and values (plural)
+function form.select ( v )
+ if ( v.name == nil ) then
+ return nil
+ end
+ local str = "<select"
+ for i,k in ipairs ( {
+ "name", "size", "tabindex", "accesskey",
+ "onfocus", "onblur", "onchange", "id",
+ "class"
+ } ) do
+ str = str .. nv_pair ( k, v[k] )
+ end
+
+ if ( v.disabled ~= nil ) then
+ str = str .. " disabled"
+ end
+ str = str .. ">"
+ -- now the options
+ for i, k in ipairs ( v.option ) do
+ local val = k
+ local txt = nil
+ if type(val) == "table" then
+ txt=val[1]
+ val=val[0]
+ end
+ str = str .. "<option "
+ if ( v.value == val ) then
+ str = str .. " selected "
+ end
+ str = str .. nv_pair("value", val) .. ">" .. k .. "</option>"
+ end
+ str = str .. "</select>"
+ return (str)
+end
+
+function form.checkbox ( v )
+ return generic_input ( "checkbox", v )
+end
+
+
+-- NOTE: VALUE of a form is a table containing the form elements ...
+function form.start ( v)
+ if ( v.action == nil ) then
+ return nil
+ end
+
+ local method = v.method or "get"
+ return ( string.format (
+ '<form %s%s%s>',
+ nv_pair ( "class", v.class ),
+ nv_pair ( "method", v.method),
+ nv_pair ( "action", v.action )
+ ) )
+end
+
+function form.stop ( )
+ return ("</form>")
+end
+
+-- For "h1, h2, p," etc
+-- WARNING - Text is printed verbatim - you may want to
+-- wrap the text in html_escape
+function entity (tag, text, class, id)
+ return ( string.format (
+ "<%s%s%s>%s</%s>",
+ tag,
+ nv_pair ("class", class),
+ nv_pair("id", id), text , tag)
+ )
+end
+
+
+function link ( v )
+ if ( v.value == nil ) then
+ return nil
+ end
+ local str = nv_pair ( "href", v.value )
+ for i,k in ipairs( { "class", "id" }) do
+ str = str .. nv_pair ( k, v[k] )
+ end
+
+ return ( "<a " .. str .. ">" .. (v.label or "" ) .. "</a>" )
+end
+
diff --git a/lib/join.lua b/lib/join.lua
new file mode 100644
index 0000000..e3621de
--- /dev/null
+++ b/lib/join.lua
@@ -0,0 +1,25 @@
+--[[
+ module for joining an array to a string
+]]--
+
+module (..., package.seeall)
+
+
+-- This code comes from http://lua-users.org/wiki/SplitJoin
+--
+-- Concat the contents of the parameter list,
+-- -- separated by the string delimiter (just like in perl)
+-- -- example: strjoin(", ", {"Anna", "Bob", "Charlie", "Dolores"})
+return function (delimiter, list)
+ local len = getn(list)
+ if len == 0 then
+ return ""
+ end
+ local string = list[1]
+ for i = 2, len do
+ string = string .. delimiter .. list[i]
+ end
+ return string
+end
+
+
diff --git a/lib/log_view.lua b/lib/log_view.lua
new file mode 100644
index 0000000..f32400e
--- /dev/null
+++ b/lib/log_view.lua
@@ -0,0 +1,56 @@
+require ("web_elements")
+
+local function fwrite(fmt, ...)
+ return io.write(string.format(fmt, ...))
+end
+
+local function footer(time)
+ fwrite("<div id=\"footer\">\n<p>This request was processed in approximately %d seconds</p>\n</div>",time)
+end
+
+header = [[
+content-type: text/html
+
+<!DOCTYPE HTML PUBLIC "-//W3C//ddD HTML 4.01 Transitional//EN">
+<html lang="en">
+
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
+<title>Alpine log view</title>
+<link rel="stylesheet" type="text/css" href="/static/webconf.css" />
+<meta http-equiv='Cache-Control' content='no-cache' />
+<Meta http-equiv='Pragma' content='no-cache' />
+</head>]]
+
+--
+
+print(header)
+print("<body>\n<div id=\"head\">")
+
+fwrite("<h1>%s</h1>",cf.hostinfo.alpine_hostname)
+
+fwrite("<p><em>%s</em></p>",cf.hostinfo.alpine_release)
+
+print("</div>")
+
+print ('<div id="mainmenu">')
+local group, cat, subcat =
+ web_elements.render_mainmenu ( menu, cf.prefix, cf.controller, cf.action )
+print([[
+</div>
+
+<div id="submenu">]])
+web_elements.render_submenu ( menu, group, cat, subcat )
+print([[</div>
+
+<div id="content">
+<p>]])
+-- get the wc and view tables
+-- walk the tree
+web_elements.render_table ( view )
+print("</p>\n</div>")
+
+print(footer(cf.time))
+print("</body></html>")
+
+-- /* vim: set filetype=lua : */
diff --git a/lib/menubuilder.lua b/lib/menubuilder.lua
new file mode 100644
index 0000000..6def1fe
--- /dev/null
+++ b/lib/menubuilder.lua
@@ -0,0 +1,150 @@
+--[[ parse through the *.menu tables and return a "menu" table
+ Written for Alpine Configuration Framework (ACF) -- see www.alpinelinux.org
+ Copyright (C) 2007 Nathan Angelacos
+ Licensed under the terms of GPL2
+ ]]--
+module(..., package.seeall)
+
+-- returns a table of the "*.menu" tables
+-- uses the system "find" command
+-- startdir should be the app dir.
+local get_candidates = function (startdir)
+ local t = {}
+ startdir = startdir .. "/"
+ local fh = io.popen('find ' .. startdir .. ' -name "*.menu"')
+
+ local start = string.gsub(startdir, "/$", "")
+ for x in fh:lines() do
+ table.insert (t, (string.gsub(x, start, "")))
+ end
+
+ return t
+end
+
+
+-- internal function for table.sort
+local t_compare = function (x,y,f)
+ for k,v in pairs(f) do
+ local a = x[v]
+ local b = y[v]
+ if tonumber(a) and tonumber(b) then
+ a=tonumber(a)
+ b=tonumber(b)
+ end
+ if a < b then return true end
+ if a > b then return false end
+ end
+ return false
+ end
+
+
+-- returns a table of all the menu items found, sorted by priority
+-- Table format: prefix controller cat group tab action
+get_menuitems = function (startdir)
+ local t = {}
+ for k,v in pairs(get_candidates(startdir)) do
+ local prefix, controller = mvc.dirname(v), mvc.basename(v, ".menu")
+ -- open the thing, and parse the contents
+ local fh = io.open(startdir .. "/" .. v)
+ local prio = 10
+ for x in fh:lines() do
+ if not string.match(x, "^#") then
+ local item = {}
+ for i in string.gmatch(x, "%S+") do
+ table.insert(item, i)
+ end
+ table.insert(t, { prefix=prefix,
+ controller=controller,
+ catprio="nan",
+ cat=item[1] or "",
+ groupprio="nan",
+ group=item[2] or "",
+ tabprio=tostring(prio),
+ tab=item[3] or "",
+ action=item[4] or "" })
+ prio=prio+5
+ end
+ end
+ fh:close()
+ end
+ -- Ok, we now have the raw menu table
+ -- now try to parse out numbers in front of any cat, group or tabs
+ for x in ipairs(t) do
+ local f = t[x]
+ if (string.match(f.cat, "^%d")) then
+ f.catprio, f.cat = string.match(f.cat, "(%d+)(.*)")
+ end
+ if (string.match(f.group, "^%d")) then
+ f.groupprio, f.group = string.match(f.group, "(%d+)(.*)")
+ end
+ if (string.match(f.tab, "^%d")) then
+ f.tabprio, f.tab = string.match(f.tab, "(%d+)(.*)")
+ end
+ end
+
+ -- Convert underscores to spaces
+ for x in ipairs(t) do
+ t[x].cat = string.gsub(t[x].cat, "_", " ")
+ t[x].group = string.gsub(t[x].group, "_", " ")
+ t[x].tab = string.gsub(t[x].tab, "_", " ")
+ end
+
+ -- Now alpha sort
+ table.sort(t, function(x,y)
+ return t_compare (x,y,{"cat", "catprio", "group", "groupprio", "tab", "tabprio"} )
+ end)
+
+ -- Fill in the priorities
+ local fill_prio = function (t, start, stop, col)
+ local prio = t[start][col]
+ if prio == "nan" then prio = "0" end
+ while start <= stop do
+ t[start][col] = prio
+ start = start + 1
+ end
+ end
+
+
+-- Fill in the priorities
+-- Warning - UGLY code ahead.
+-- Basic rules, for each cat and group, if the prio is nan, then set it
+-- to the lowest value for that group or cat.
+ local k = 1
+ while ( k <= table.maxn(t) ) do
+ local c = k
+ while ( c <= table.maxn(t) and t[c].cat == t[k].cat ) do
+ c=c+1
+ end
+ c=c-1 -- back up one - we only want whats the same
+ fill_prio(t,k,c,"catprio")
+ -- from k,c is a mini table, do the same for groupprio
+ local g = k
+ while ( g <= c ) do
+ local h = g
+ while ( h <= c and t[h].group == t[g].group ) do
+ h=h+1
+ end
+ h=h-1 --- back up one (again)
+ fill_prio(t,g,h,"groupprio")
+ g=h+1
+ end
+ k = c + 1
+ end
+
+ -- Now priority sort
+ table.sort(t, function(x,y)
+ return t_compare (x,y,{"catprio", "cat", "groupprio", "group", "tabprio", "tab"} )
+ end)
+
+
+ -- drop the priorities - they were internal
+ for k,v in ipairs(t) do
+ v.catprio = nil
+ v.groupprio = nil
+ v.tabprio = nil
+ end
+
+ return t
+end
+
+
diff --git a/lib/service_controller.lua b/lib/service_controller.lua
new file mode 100644
index 0000000..09595a4
--- /dev/null
+++ b/lib/service_controller.lua
@@ -0,0 +1,189 @@
+local function build_status(model, name)
+ return {
+ type = "form",
+ method = "post",
+ action = cf.uri .. "/chrun",
+ value = {
+ {
+ type = "formtext",
+ name = "status",
+ label = "Status: <B>" .. name .. "</B>",
+ value = model.status(name)
+ },
+ {
+ type = "hidden",
+ name = "service",
+ value = name
+ },
+ {
+ type = "submit",
+ name = "cmd",
+ label = "Running controls",
+ value = { "Start", "Stop", "Restart" }
+ },
+ }
+ }
+end
+
+local function build_note(model, name)
+ return {
+ type = "form",
+ method = "post",
+ action = cf.uri .. "/note_set",
+ value = {
+ {
+ type = "textarea",
+ name = "note",
+ label = "Notes",
+ cols = 80,
+ rows = 8,
+ value = model.get_note(name)
+ },
+ {
+ type = "submit",
+ label = "Command",
+ value = "Save"
+ }
+ }
+ }
+end
+
+local function mkerr(str)
+ return {
+ {
+ type = "formtext",
+ class = "error",
+ value = str
+ }
+ }
+end
+
+local function mkresult(str)
+ return {
+ {
+ type = "formtext",
+ value = str
+ }
+ }
+end
+
+local function chrun(model, name, ucmd)
+ local cmd = string.lower(ucmd)
+ --TODO chect that name is in get_service_names()
+ if cmd ~= "start" and cmd ~= "stop" and cmd ~= "restart" then
+ return mkerr("unknown command")
+ end
+ return mkresult(model.initd(name, cmd))
+end
+
+local function build_log_file(model, name)
+ return {
+ type = "group",
+ label = name,
+ text = "FILE " .. model.get_log_names()[name].path,
+ value = {
+ {
+ type = "log",
+ lines = model.get_log_producer(name)
+ }
+ }
+ }
+end
+
+local function build_edit_file(model, name)
+ return {
+ type = "form",
+ method = "post",
+ action = cf.uri .. "/cfg_set",
+ value = {
+ {
+ type = "textarea",
+ name = "cfg",
+ label = name,
+ cols = 80,
+ rows = 12,
+ value = model.get_cfg(name)
+ },
+ {
+ type = "hidden",
+ name = "name",
+ value = name
+ },
+ {
+ type = "submit",
+ label = "Command",
+ value = "Save"
+ }
+ }
+ }
+
+end
+
+local function build_cfg(model)
+ local ret = {}
+ for name, whatever in pairs(model.get_cfg_names()) do
+ table.insert(ret, build_edit_file(model, name))
+ end
+ return ret
+end
+
+local function set_cfg(model)
+ for name, whatever in pairs(model.get_cfg_names()) do
+ if name == FORM.name then
+ model.set_cfg(name, FORM.cfg)
+ return
+ end
+ end
+ return mkerr("unknown config")
+end
+
+local function build_log(model)
+ local ret = {}
+ for name, whatever in pairs(model.get_log_names()) do
+ table.insert(ret, build_log_file(model, name))
+ end
+ return ret
+end
+
+local function build_service(model)
+ local ret = {}
+ for name, whatever in pairs(model.get_service_names()) do
+ table.insert(ret, build_status(model, name))
+ end
+ table.insert(ret, build_note(model))
+ return ret
+end
+
+function create_service_controller()
+
+ local me = {}
+
+ me.service = build_service
+ me.default = me.service
+ me.cfg = build_cfg
+ me.log = build_log
+
+ function me.chrun(model)
+ return chrun(model, FORM.service, FORM.cmd)
+ end
+
+ function me.note_set(model)
+ local ret = model.set_note(FORM.note)
+ if ret then
+ return ret
+ end
+ return me.service(model)
+ end
+
+
+ function me.cfg_set(model)
+ set_cfg(model)
+ return me.cfg(model)
+ end
+
+ return me
+
+end
+
+-- /* vim: set filetype=lua shiftwidth=4: */
+
diff --git a/lib/service_model.lua b/lib/service_model.lua
new file mode 100644
index 0000000..c619d61
--- /dev/null
+++ b/lib/service_model.lua
@@ -0,0 +1,115 @@
+require("fs")
+
+function create_service_model(cfglist, loglist, servlist, notepath)
+
+ local me = {}
+
+ local function get_any_pid(pname)
+ for e in lfs.dir("/proc") do
+ if e == string.match(e, "^%d*$") then
+ for line in io.lines("/proc/" .. e .. "/status") do
+ tag, val = string.match(line, "^([^:]*):%s*(%S*)$");
+ if tag == "Name" then
+ if val == pname then return e end
+ break
+ end
+ end
+ end
+ end
+ end
+
+ local function get_cfg_path(name)
+ if not cfglist or not cfglist[name] then
+ return
+ end
+ return cfglist[name].path
+ end
+
+ local function get_log_path(name)
+ if not loglist or not loglist[name] then
+ return
+ end
+ return loglist[name].path
+ end
+
+ --[[ Public Functions go here ]]--
+
+ function me.status(name)
+ if not servlist or not servlist[name] then
+ return "unknown service"
+ end
+ local cmdname = servlist[name].cmdname
+ if get_any_pid(cmdname) == nil then
+ return "seems to be stopped"
+ else
+ return "seems to be running"
+ end
+ end
+
+ function me.initd(name, action)
+ if not servlist or not servlist[name] then
+ return "unknown service"
+ end
+ local p = io.popen("/etc/init.d/"
+ .. servlist[name].initdname .. " " .. action, "r")
+ local ret = p:read("*a")
+ p:close()
+ return ret
+ end
+
+ function me.get_note()
+ if not notepath or not fs.is_file(notepath) then return "" end
+ return fs.read_file(notepath)
+ end
+
+ function me.set_note(value)
+ if notepath == nil then return end
+ fs.write_file(notepath, value)
+ end
+
+ function me.get_cfg_names()
+ return cfglist
+ end
+
+ function me.get_log_names()
+ return loglist
+ end
+
+ function me.get_service_names()
+ return servlist
+ end
+
+ function me.get_cfg(name)
+ local path = get_cfg_path(name)
+ if not path or not fs.is_file(path) then
+ return ""
+ end
+ return fs.read_file(path)
+ end
+
+ function me.set_cfg(name, value)
+ local path = get_cfg_path(name)
+ if path then
+ fs.write_file(path, value)
+ end
+ end
+
+-- local function wrap_single(x)
+-- return function(state, prev)
+-- if not prev then
+-- return x
+-- end
+-- end
+-- end
+
+ function me.get_log_producer(name)
+ local path = get_log_path(name)
+ if not path or not fs.is_file(path) then
+ return "cannot access " .. path
+ end
+ return io.lines(path)
+ end
+
+ return me
+
+end
diff --git a/lib/session.lua b/lib/session.lua
new file mode 100644
index 0000000..f23f5a5
--- /dev/null
+++ b/lib/session.lua
@@ -0,0 +1,143 @@
+-- Session handling routines - written for acf
+-- Copyright (C) 2007 N. Angelacos - GPL2 License
+
+module (..., package.seeall)
+
+local b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
+
+-- Return a sessionid of at least size bits length
+random_hash = function (size)
+ local file = io.open("/dev/urandom")
+ local str = ""
+ if file == nil then return nil end
+ while (size > 0 ) do
+ local offset = (string.byte(file:read(1)) % 64) + 1
+ str = str .. string.sub (b64, offset, offset)
+ size = size - 6
+ end
+ return str
+end
+
+hash_ip_addr = function (string)
+ local str = ""
+ for i in string.gmatch(string, "%d+") do
+ str = str .. string.format("%02x", i )
+ end
+ return str
+end
+
+ip_addr_from_hash = function (string)
+ local str = ""
+ for i in string.gmatch(string, "..") do
+ str = str .. string.format("%d", "0x" .. i) .. "."
+ end
+ return string.sub(str, 1, string.len(str)-1)
+end
+
+
+--[[
+ These functions serialize a table, including nested tables.
+ The code based on code in PiL 2nd edition p113
+]]--
+local function basicSerialize (o)
+ if type(o) == "number" then
+ return tostring(o)
+ else
+ return string.format("%q", o)
+ end
+end
+
+
+function serialize (name, value, saved )
+ local str = str or ""
+ saved = saved or {}
+ str = str .. name .. " = "
+ if type(value) == "number" or type(value) == "string" then
+ str = str .. basicSerialize (value) .. "\n"
+ elseif type(value) == "table" then
+ if saved[value] then
+ str = str .. saved[value] .. "\n"
+ else
+ saved[value] = name
+ str = str .. "{}\n"
+ for k,v in pairs(value) do
+ local fieldname = string.format("%s[%s]", name, basicSerialize(k))
+ str = str .. serialize (fieldname, v, saved)
+ end
+ end
+ elseif type(value) == "boolean" then
+ str = str .. tostring(value) .. "\n"
+ else
+ str = str .. "nil\n" -- cannot save other types, so skip them
+ end
+ return str
+end
+
+save_session = function( sessionpath, session, sessiontable)
+ local file = io.open(sessionpath .. "/" .. session , "w")
+ if file then
+ file:write ( "-- This is an ACF session table.\nlocal timestamp=" .. os.time() )
+ file:write ( "\nlocal " )
+ file:write ( serialize("s", sessiontable) )
+ file:write ( "return timestamp, s\n")
+ file:close()
+ return true
+ else
+ return false
+ end
+end
+
+
+--- FIXME: This is really a generic "test if file exists" thing.
+
+-- Tests if a session is valid
+-- Returns true if valid, false if not
+session_is_valid = function (session)
+ local file = io.open(session)
+ if file then
+ file:close()
+ return true
+ else
+ return false
+ end
+end
+
+-- Loads a session
+-- Returns a timestamp (when the session data was saved) and the session table.
+load_session = function ( sessionpath, session )
+ -- session can only have b64 characters in it
+ session = string.gsub ( session, "[^" .. b64 .. "]", "")
+ if #session == 0 then
+ return nil, {}
+ end
+ session = sessionpath .. "/" .. session
+ if (session_is_valid(session)) then
+ local file = io.open(session)
+ return dofile(session)
+ else
+ return nil, {}
+ end
+end
+
+-- unlinks a session
+invalidate_session = function (sessionpath, session)
+ if type(session) ~= "string" then return nil end
+ local s = string.gsub (session, "[^" .. b64 .. "]", "")
+ if s ~= session then
+ return nil
+ end
+ session = sessionpath .. "/" .. s
+ os.remove (session)
+ return nil
+end
+
+
+expire_old_sessions = function ( sessiondir )
+ file = io.popen("ls " .. sessiondir )
+ x = file:read("*a")
+ file:close()
+ for a in string.gmatch(x, "[^%c]+") do
+ timestamp, foo = load_session ( sessiondir .. "/" .. a )
+ print ( a .. " is " .. os.time() - timestamp .. " seconds old")
+ end
+end
diff --git a/lib/split.lua b/lib/split.lua
new file mode 100644
index 0000000..f3e2e95
--- /dev/null
+++ b/lib/split.lua
@@ -0,0 +1,31 @@
+--[[
+ module for splitting a string to an array
+]]--
+
+module (..., package.seeall)
+
+-- This code comes from http://lua-users.org/wiki/SplitJoin
+
+-- Split text into a list consisting of the strings in text,
+-- separated by strings matching delimiter (which may be a pattern).
+-- example: strsplit(",%s*", "Anna, Bob, Charlie,Dolores")
+return function (delimiter, text)
+ local list = {}
+ local pos = 1
+ -- this would result in endless loops
+ if string.find("", delimiter, 1) then
+ error("delimiter matches empty string!")
+ end
+ while 1 do
+ local first, last = string.find(text, delimiter, pos)
+ if first then -- found?
+ table.insert(list, string.sub(text, pos, first-1))
+ pos = last+1
+ else
+ table.insert(list, string.sub(text, pos))
+ break
+ end
+ end
+ return list
+end
+
diff --git a/lib/validator.lua b/lib/validator.lua
new file mode 100755
index 0000000..951d716
--- /dev/null
+++ b/lib/validator.lua
@@ -0,0 +1,156 @@
+--------------------------------------------------
+-- Validation Functions for Alpine Linux' Webconf
+--------------------------------------------------
+
+-- setup the 'language' table
+lang = {}
+lang.English = 1
+lang.German = 2
+lang.French = 3
+lang.Current = lang.English
+
+-- setup the 'validator' tables
+validator = {}
+validator.msg = {}
+validator.msg.err = {}
+
+-- setup error messages
+validator.msg.err.Success = {}
+validator.msg.err.Success[lang.English] = "Ok."
+validator.msg.err.Success[lang.German] = "Ok."
+validator.msg.err.InvalidChars = {}
+validator.msg.err.InvalidChars[lang.English] = "Invalid characters!"
+validator.msg.err.InvalidChars[lang.German] = "Ungültige Zeichen!"
+validator.msg.err.InvalidLength = {}
+validator.msg.err.InvalidLength[lang.English] = "Invalid length!"
+validator.msg.err.InvalidLength[lang.German] = "Ungültige Länge!"
+validator.msg.err.InvalidFormat = {}
+validator.msg.err.InvalidFormat[lang.English] = "Invalid format!"
+validator.msg.err.InvalidFormat[lang.German] = "Ungültiges Format!"
+validator.msg.err.InvalidValue = {}
+validator.msg.err.InvalidValue[lang.English] = "Invalid Value!"
+validator.msg.err.InvalidValue[lang.German] = "Ungültiger Wert!"
+validator.msg.err.OutOfRange = {}
+validator.msg.err.OutOfRange[lang.English] = "Value out of range!"
+validator.msg.err.OutOfRange[lang.German] = "Wert ausserhalb des Bereichs!"
+
+--
+-- This function validates an ipv4 address.
+-- On success it returns 1 otherwise a negative value
+--
+function validator.is_ipv4(ipv4)
+ local retval = false;
+ local nums = { "", "", "", ""};
+ local iplen = string.len(ipv4);
+
+ -- check the ipv4's length
+ if (iplen < 7 or iplen > 15) then
+ return false, validator.msg.err.InvalidLength[lang.Current]
+ end
+
+ -- NC: Split the string into an array. separate with '.' (dots)
+ -- ^ beginning of string
+ -- () capture
+ -- \. litteral '.' The \ neutralizes the . character class.
+ -- %d+ one or more digits
+ -- $ end of string
+ nums = { ipv4:match ("^(%d+)\.(%d+)\.(%d+)\.(%d+)$" ) }
+
+ -- check if all nums are filled
+ if ( nums[1] == nil or
+ nums[2] == nil or
+ nums[3] == nil or
+ nums[4] == nil) then
+ -- we have an empty number
+ return false, validator.msg.err.InvalidFormat[lang.Current]
+ end
+
+ -- too big?
+ if (tonumber(nums[1]) > 255 or
+ tonumber(nums[2]) > 255 or
+ tonumber(nums[3]) > 255 or
+ tonumber(nums[4]) > 255) then
+ -- at least one number is too big
+ return false, validator.msg.err.InvalidValue[lang.Current]
+ end
+
+ return true, validator.msg.err.Success[lang.Current]
+end
+
+function validator.is_mac(mac)
+
+ local tmpmac = string.upper(mac)
+
+ if (string.len(tmpmac) ~= 17) then
+ return false, validator.msg.err.InvalidLength[lang.Current]
+ end
+
+ -- check for valid characters
+ local step = 1;
+ while (step <= 17) do
+ if (string.sub(tmpmac, step, step) ~= ":") and
+ (string.sub(tmpmac, step, step) < "0" or string.sub(tmpmac, step, step) > "9") and
+ (string.sub(tmpmac, step, step) < "A" or string.sub(tmpmac, step, step) > "F") then
+ -- we have found an invalid character!
+ return false, validator.msg.err.InvalidChars[lang.Current]
+ end
+ step = step + 1;
+ end
+
+ -- check for valid colon positions
+ if (string.sub(tmpmac, 3, 3) ~= ":" or
+ string.sub(tmpmac, 6, 6) ~= ":" or
+ string.sub(tmpmac, 9, 9) ~= ":" or
+ string.sub(tmpmac, 12, 12) ~= ":" or
+ string.sub(tmpmac, 15, 15) ~= ":") then
+ return false, validator.msg.err.InvalidFormat[lang.Current]
+ end
+
+ -- check for valid non colon positions
+ step = 1;
+ while (step <= 17) do
+ if ((string.sub(tmpmac, step, step) == ":") and
+ ((step ~= 3) and (step ~= 6) and (step ~= 9) and (step ~= 12) and
+ (step ~= 15))) then
+ return false, validator.msg.err.InvalidValue[lang.Current]
+ end
+ step = step + 1;
+ end
+
+ return true, validator.msg.err.Success[lang.Current]
+end
+
+--
+-- This function checks if the given input
+-- consists of number-chars between 0..9 only
+-- and eventually a leading '-'
+--
+function validator.is_integer(numstr)
+ -- ^ beginning of string
+ -- -? one or zero ot the char '-'
+ -- %d+ one or more digits
+ -- $ end of string
+ return string.find(numstr, "^-?%d+$") ~= nil
+end
+
+
+--
+-- This function checks if the given input
+-- consists of number-chars between 0..9 only
+-- and if it is within a given range.
+--
+function validator.is_integer_in_range(numstr, min, max)
+ return validator.is_integer(numstr)
+ and numstr >= min
+ and numstr <= max
+
+end
+
+--
+-- This function checks if the given number is an integer
+-- and wheter it is between 1 .. 65535
+--
+function validator.is_port(numstr)
+ return validator.is_integer_in_range(numstr, 1, 65535)
+end
+
diff --git a/lib/web_elements.lua b/lib/web_elements.lua
new file mode 100644
index 0000000..e676205
--- /dev/null
+++ b/lib/web_elements.lua
@@ -0,0 +1,156 @@
+--[[
+ middle level functions for the webconf view templates. Part of
+ acf
+
+ Copyright (C) 2006 N. Angelacos Licensed under terms of GPL2
+]]--
+
+module ( ..., package.seeall )
+
+require ("html")
+
+-- This is the main function that walks a table formatted for a template
+-- (This is the magic in generic)
+function render_table ( element, level)
+ local level = level or 1
+
+ if (type(element) ~= "table" ) then
+ return nil
+ end
+
+ for k,v in pairs (element) do
+ if ( v.type ~= nil ) then
+ if ( v.type == "group" ) then
+ print ( html.entity ( ( "h" .. tostring(level) ), v.label, v.class, v.id ) )
+ print ( html.entity ( "p" , v.text, v.class ) )
+ render_table ( v.value, level + 1 )
+ elseif ( v.type == "label" ) then
+ print ( html.entity ( "h" .. level , v.value, v.class, v.id ) )
+ if ( v.text ~= nil ) then
+ print ( html.entity ( "p", v.text, v.class ))
+ end
+ elseif ( v.type == "html" ) then
+ print (v.value)
+ elseif ( v.type == "log" ) then
+ print("<pre>")
+ if type(v.lines) == "function" then
+ for line in v.lines do
+ print(line)
+ end
+ elseif v.lines then
+ print(v.lines)
+ end
+ print("</pre>")
+ elseif ( v.type == "link" ) then
+ print (html.link ( v ) )
+ elseif ( v.type == "form" ) then
+ print ( html.form.start ( v ) )
+ print ("<dl>")
+ render_table ( v.value, level + 1 )
+ print ("</dl>")
+ print ( html.form.stop () )
+ elseif type(html.form[v.type]) ~= "nil" then
+ if v.type == "hidden" then
+ v.noitem = true
+ end
+ if not v.noitem then
+ print ( string.format ("<dt>%s</dt><dd>", ( v.label or "" )))
+ end
+ print ( html.form[v.type] ( v ) )
+ if not v.noitem then
+ print ("</dd>")
+ end
+ end
+ end
+ end
+end
+
+
+-- This function prints the main menu, with the given prefix/controller "selected"
+-- returns the group, category, subcat that is selected
+function render_mainmenu ( menu, prefix, controller, action )
+ -- prefix+controller defines which menu is "selected"
+ local megroup = nil
+ local mecat = nil
+ local mesubcat = nil
+ local liston = nil
+ local group = ""
+ local cat = ""
+
+ -- find the current group/cat/subcat
+ for i=1,table.maxn(menu) do
+ if (menu[i].prefix == prefix) and ( menu[i].controller == controller ) then
+ megroup = menu[i].group
+ mecat = menu[i].cat
+ if ( menu[i].action == action ) then
+ mesubcat = menu[i].subcat
+ elseif ( menu[i].action == "*" ) and ( mesubcat == nil ) then
+ mesubcat = menu[i].subcat
+ end
+ end
+ end
+
+ -- render the mainmenu
+ local thisgroup = ""
+ local thiscat = ""
+ for i=1,table.maxn(menu),1 do
+ if menu[i].group ~= thisgroup then
+ thisgroup = menu[i].group
+ if ( liston ) then io.write ("</ul>") end
+ print ( html.entity ( "h3", menu[i].group ) )
+ io.write("<ul>")
+ liston = true
+ thicat = nil
+ end
+ if menu[i].cat ~= thiscat then
+ thiscat = menu[i].cat
+ if (thiscat == mecat ) then
+ print ( html.entity ("li", html.html_escape(thiscat), nil, "selected"))
+ else
+ print (html.link ( { value= ENV.SCRIPT_NAME .. menu[i].uri ,
+ label = html.entity ("li", html.html_escape(thiscat)) } ) )
+ end
+ end
+ end
+ io.write ("</ul>")
+ return megroup, mecat, mesubcat
+end
+
+
+
+-- This function prints the tabs for the submenu, with the given submenu "selected"
+function render_submenu ( menu, group, cat, subcat )
+ cat = cat or ""
+ group = group or ""
+ local this_subcat = nil
+ local foo = group .. " > " .. cat
+ if (foo ~= " > " ) then
+ print ( html.entity ( "h2", html.html_escape( group .. " > " .. cat ) ))
+ end
+
+
+ -- print (string.format ("%s - %s - %s", group, cat , (subcat or "")))
+
+ io.write ("<ul>")
+ for i=1, table.maxn(menu),1 do
+ if ( group == menu[i].group ) and ( cat == menu[i].cat ) then
+ -- If a subcat was not selected, make the first one the default
+ if ( subcat == nil ) then
+ subcat = menu[i].subcat
+ end
+
+ if ( menu[i].subcat ~= this_subcat ) then
+ this_subcat = menu[i].subcat
+ if ( menu[i].subcat == subcat ) then
+ print ( html.entity ("li", menu[i].subcat, nil, "selected"))
+ else
+ io.write ("<li>")
+ io.write ( html.link ( { value= ENV.SCRIPT_NAME .. menu[i].uri ,
+ label =menu[i].subcat } ) )
+ print ("</li>")
+ end
+ end
+ end
+ end
+ io.write ("</ul>")
+end
diff --git a/www/Makefile b/www/Makefile
new file mode 100644
index 0000000..e208bdf
--- /dev/null
+++ b/www/Makefile
@@ -0,0 +1,50 @@
+include ../config.mk
+
+WWW_DIST=cgi-bin/acf\
+ cgi-bin/mvc.lua\
+ static/alpine.gif\
+ static/alpine.png\
+ static/arctic-back.png\
+ static/arctic-upleft.png\
+ static/greypx.gif\
+ static/selected.gif\
+ static/submenu.css\
+ static/unselected.gif\
+ static/webconf-purple.css\
+ static/webconf.css\
+ index.html\
+ sample.html
+
+EXTRA_DIST=Makefile
+DISTFILES=$(WWW_DIST) $(EXTRA_DIST)
+
+install_dir=$(DESTDIR)/$(wwwdir)
+dist_dir=$(DISTDIR)/$(notdir $(PWD))
+
+phony+=all
+all:
+
+phony+=clean
+clean:
+
+phony+=distdir
+distdir: $(DISTFILES)
+ mkdir -p "$(dist_dir)"
+ for i in $(DISTFILES); do\
+ dest=`dirname "$(dist_dir)/$$i"`;\
+ mkdir -p "$$dest";\
+ cp "$$i" "$$dest";\
+ done
+
+phony+=install
+install:
+ mkdir -p $(install_dir)
+ for i in $(WWW_DIST); do\
+ dest=`dirname "$(install_dir)/$$i"`;\
+ mkdir -p "$$dest";\
+ cp "$$i" "$$dest";\
+ done
+ chmod +x $(install_dir)/cgi-bin/acf
+
+
+.PHONY: $(phony)
diff --git a/www/cgi-bin/acf b/www/cgi-bin/acf
new file mode 100755
index 0000000..b52f9e7
--- /dev/null
+++ b/www/cgi-bin/acf
@@ -0,0 +1,19 @@
+#!/usr/bin/haserl --shell=lua -a
+<?
+require("mvc")
+
+-- create a new container
+FRAMEWORK=mvc:new()
+
+-- set the configuration parameters
+-- This loads the container with the config info
+-- but does not load the application worker/model
+FRAMEWORK:read_config("acf")
+
+-- Create an application container -
+-- loads the application controller/model code
+APP=FRAMEWORK:new("acf_www")
+
+-- Dispatch the application
+APP:dispatch()
+?>
diff --git a/www/cgi-bin/mvc.lua b/www/cgi-bin/mvc.lua
new file mode 100755
index 0000000..b677d3b
--- /dev/null
+++ b/www/cgi-bin/mvc.lua
@@ -0,0 +1,278 @@
+--[[ Basic MVC framework
+ Written for Alpine Configuration Framework (ACF) -- see www.alpinelinux.org
+ Copyright (C) 2007 Nathan Angelacos
+ Licensed under the terms of GPL2
+ ]]--
+module(..., package.seeall)
+
+-- the constructor
+--[[ Builds a new MVC object. If "module" is given, then tries to load
+ self.conf.appdir .. module "-controller.lua" in c.worker and
+ self.conf.appdir .. module "-model.lua" in c.model
+
+ The returned .conf table is guaranteed to have the following
+ appdir - where the application lives
+ confdir - where the configuration file is
+ sessiondir - where session data and other temporary stuff goes
+ appname - the name of the application
+ ]]
+
+new = function (self, modname)
+ local c = {}
+ c.worker = {}
+ c.model = {}
+
+ -- make defaults if the parent doesn't have them
+ if self.conf == nil then
+ c.conf = { appdir = "", confdir = "", tempdir = "", appname = "" }
+ end
+
+ -- If no clientdata, then clientdata is a null table
+ if self.clientdata == nil then
+ c.clientdata = {}
+ end
+
+ -- If we don't have an application name, use the modname
+ if (self.conf == nil ) or (self.conf.appname == nil) then
+ c.conf.appname = modname
+ end
+
+ -- load the module code here
+ if (modname) then
+ c.worker = self:soft_require( modname .. "-controller") or {}
+ c.model = self:soft_require( modname .. "-model" ) or {}
+ end
+
+ -- The magic that makes all the metatables point in the correct
+ -- direction. c.model -> c.worker -> parent -> parent.worker ->
+ -- grandparent -> grandparent -> worker (and so on)
+
+ -- The model looks in worker for missing
+ setmetatable (c.model, c.model )
+ c.model.__index = c.worker
+
+ -- the worker looks in the main table for missing
+ setmetatable (c.worker, c.worker)
+ c.worker.__index = c
+
+ -- the table looks in the parent worker for missing
+ setmetatable (c, c)
+
+ -- ensure an "mvc" table exists, even if empty
+ if (type(rawget(c.worker, "mvc")) ~= "table") then
+ c.worker.mvc = {}
+ end
+
+ setmetatable (c.worker.mvc, c.worker.mvc)
+ -- If creating a new parent container, then
+ -- we are the top of the chain.
+ if (modname) then
+ c.__index = self.worker
+ c.worker.mvc.__index = self.worker.mvc
+ else
+ c.__index = self
+ c.worker.mvc.__index = self.mvc
+ end
+
+
+ -- run the worker on_load code
+ if type(rawget(c.worker.mvc, "on_load")) == "function" then
+ c.worker.mvc.on_load(c, self)
+ c.worker.mvc.on_load = nil
+ end
+
+ return c
+end
+
+-- This is a sample front controller/dispatch.
+dispatch = function (self)
+ local controller
+ local success, err = xpcall ( function ()
+
+ self:parse_path_info(ENV["PATH_INFO"])
+
+ -- If they didn't provide a controller, and a default was specified
+ -- use it
+ if self.conf.controller == "" and self.conf.default_controller then
+ self.conf.controller = self.conf.default_controller
+ end
+
+ controller = self:new(self.conf.prefix .. self.conf.controller)
+
+ local action = controller.conf.action
+
+ -- Because of the inheritance, normally the
+ -- controller.worker.action will flow up, so that EVERY
+ -- worker has an "exception_handler" action. We use rawget to
+ -- make sure that only controller defined actions are used.
+ -- If the controller or action are missing, raise an error
+ if ( type(rawget(controller.worker, action)) ~= "function") then
+ self.conf.type = "dispatch"
+ error (self.conf)
+ end
+
+ -- run the pre_exec code
+--- if type(rawget(controller.worker.mvc, "pre_exec")) == "function" then
+ if type(controller.worker.mvc.pre_exec) == "function" then
+ controller.worker.mvc.pre_exec ( controller )
+ end
+
+ -- run the action
+ local viewtable = controller.worker[action](controller)
+
+
+ -- run the post_exec code
+ if type(controller.worker.mvc.post_exec) == "function" then
+ -- if type(controller.worker.mvc, "post_exec") == "function" then
+ controller.worker.mvc.post_exec ( controller )
+ end
+
+
+ local viewfunc = controller:view_resolver(viewtable)
+
+ viewfunc (viewtable)
+ end,
+ self:soft_traceback(message)
+ )
+
+ if not success then
+ local handler
+ if controller then
+ handler = controller.worker or controller
+ end
+ handler = handler or self.worker or mvc
+ handler:exception_handler(err)
+ end
+end
+
+
+-- Tries to see if name exists in the self.conf.appdir, and if so, it loads it.
+-- otherwise, returns nil, but no error
+soft_require = function (self, name )
+ local filename, file
+ filename = self.conf.appdir .. name .. ".lua"
+ file = io.open(filename)
+ if file then
+ file:close()
+ local PATH=package.path
+ -- FIXME - this should really try to open the lua file, and if it doesnt exist-
+ -- then silently fail -- This version allows things from /usr/local/lua/5.1 to
+ -- be loaded - but that's because a module might require another module
+ package.path = self.conf.appdir .. "?" .. ".lua;" .. package.path
+ local t = require(name)
+ package.path = PATH
+ return t
+ end
+ return nil
+end
+
+-- see man basename.1
+basename = function (string, suffix)
+ string = string or ""
+ local basename = string.gsub (string, "[^/]*/", "")
+ if suffix then
+ basename = string.gsub ( basename, suffix, "" )
+ end
+ return basename
+end
+
+-- see man dirname.1
+dirname = function ( string)
+ string = string or ""
+ -- strip trailing / first
+ string = string.gsub (string, "/$", "")
+ local basename = basename ( string)
+ string = string.sub(string, 1, #string - #basename - 1)
+ return(string)
+end
+
+-- look in various places for a config file, and store it in self.conf
+read_config = function( self, appname )
+ appname = appname or self.conf.appname
+ self.conf.appname = self.conf.appname or appname
+
+ local confs = { (ENV["HOME"] or ENV["PWD"] or "") .. "/." .. appname .. "/" .. appname .. ".conf",
+ ( ENV["HOME"] or ENV["PWD"] or "") .. "/" .. appname .. ".conf",
+ -- dirname(ENV["SCRIPT_FILENAME"] or "") .. appname .. ".conf",
+ ENV["ROOT"] or "" .. "/etc/" .. appname .. "/" .. appname .. ".conf",
+ ENV["ROOT"] or "" .. "/etc/" .. appname .. ".conf"
+ }
+ for i, filename in ipairs (confs) do
+ local file = io.open (filename)
+ if (file) then
+ self.conf.confdir = dirname(filename) .. "/"
+ for line in file:lines() do
+ key, value = string.match(line, "([^[=]*)=[ \t]*(.*)")
+ if key then -- ugly way of finding blank spots between key and =
+ repeat
+ local space = string.find ( key, "%s", -1)
+ if space then key=string.sub(key,1,space-1) end
+ until space == nil
+ self.conf[key] = value
+ end
+ end
+ file:close()
+ break
+ end
+ end
+end
+
+-- parse a "URI" like string into a prefix, controller and action
+-- store them (or blanks) in self.conf
+parse_path_info = function( self, string )
+ string = string or ""
+ -- If it ends in a /, then add another to force
+ -- a blank action (the user gave a controller without action)
+ if string.match (string, "[^/]/$" ) then
+ string = string .. "/"
+ end
+ self.conf.action = basename(string)
+ local temp = dirname(string)
+ self.conf.controller = basename(temp)
+ self.conf.prefix = dirname(temp) .. "/"
+ return self.conf.prefix, self.conf.controller, self.conf.action
+
+end
+
+-- The View resolver of last resort.
+view_resolver = function(self)
+ return function()
+ if ENV["PATH_INFO"] then
+ io.write ("Content-type: text/plain\n\n")
+ end
+ io.write ("Your controller and application did not specify a view resolver.\n")
+ io.write ("The MVC framework has no view available. sorry.\n")
+ return
+ end
+end
+
+-- Generates a debug.traceback if called with no arguments
+soft_traceback = function (self, message )
+ if message then
+ return message
+ else
+ return debug.traceback
+ end
+end
+
+-- The exception hander of last resort
+exception_handler = function (self, message )
+ if ENV["PATH_INFO"] then
+ print ("Content-Type: text/html\n\n<pre>")
+ end
+ print ("The following unhandled application error occured:\n\n")
+
+ if (type(message) == "table" ) then
+ if (message.type == "dispatch") then
+ print ('controller: "' .. message.controller .. '" does not have a "' ..
+ message.action .. '" action.')
+ else
+ print ("An error of type: '" .. (tostring(message.type) or "nil") .. "' was raised." )
+ end
+ else
+ print (tostring(message))
+ end
+ if ENV["PATH_INFO"] then
+ print ("</pre>")
+ end
+end
diff --git a/www/index.html b/www/index.html
new file mode 100644
index 0000000..8db9a9d
--- /dev/null
+++ b/www/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns='http://www.w3.org/1999/xhtml'>
+<head>
+<meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
+<meta http-equiv='Cache-Control' content='no-cache' />
+<Meta http-equiv='Pragma' content='no-cache' />
+<meta http-equiv='refresh' content='0; url=/cgi-bin/acf'>
+</head>
+
+<body>
+</body>
diff --git a/www/sample.html b/www/sample.html
new file mode 100644
index 0000000..665334d
--- /dev/null
+++ b/www/sample.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//ddD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+<title>Alpine</title>
+<link rel="stylesheet" type="text/css" href="static/webconf.css">
+<meta http-equiv="Refresh" content="5; url=sample.html">
+</head>
+<body>
+
+
+
+
+<div id=head>
+<h1>Alpine</h1>
+<p>Host: <em>Inside</em></p>
+</div>
+
+
+<div id="mainmenu">
+<h3>Networking</h3>
+<ul><a href="foo"><li>Interfaces</li></a>
+<li id="selected"><a href="bar">Routes</a></li>
+<li><a href="bar">DNS Cache</a></li>
+<li><a href="bar">DNS Server</a></li>
+<li><a href="bar">Firewall</a></li>
+</ul>
+<h3>VPN</h3>
+<ul>
+<li><a href="foo">IPSec</a></li>
+<li><a href="bar">OpenVPN</a></li>
+<li><a href="bar">Tinc</a></li>
+</ul>
+<h3>Application Proxy</h3>
+<ul>
+<li><a href="foo">DNS Proxy</a></li>
+<li><a href="bar">Fetchmail</a></li>
+<li><a href="bar">Squid</a></li>
+</ul>
+</div>
+
+
+<div id="submenu">
+<h2>Networking > Routes</h2>
+<ul><li><a href="foo">Status</a></li>
+<li id="selected">Interfaces</li>
+<li><a href="fee">Expert</a></li>
+</ul>
+</div>
+
+
+
+<div id="content">
+<h1>Errors</h1>
+<p class="error">This is error text. As if you didn't know that. Just check the errors below.</p>
+
+<p>This is a page with a <a href="foo">link</a> in it.</p>
+
+<h1>Networking - Status</h1>
+<p>This is the networking page. This is a very long paragraph in that page, talking about
+all the good things on this page. This is not the real page, its just a standin page for
+the page you want. Evenutally, you'll see the page you want. But not quite yet.</p>
+
+<h2>Status</h2>
+<p>The service is running</p>
+<form>
+<dl>
+<dt>bozo</dt>
+<dd><input class=error type=text value="Baz" name=bozo size=3>This is error text</dd>
+
+<dt>foo</dt><dd>
+<input type=text value="foo" name=foo size=40>
+</dd>
+
+<dt>Command</dt>
+<dd>
+<input type=submit value="Start" name="cmd">
+<input type=submit value="Stop" name="cmd">
+<input type=submit value="Restart" name="cmd">
+</dd>
+
+<dt>Textbox</dt>
+<dd><textarea cols=80 rows=10 name="textbox">fooo</textarea>
+</dd>
+
+
+<dt>A Tag with no form element</dt>
+<dd>This is just some text</dd>
+
+</dl>
+</form>
+
+<h2>Configure</h2>
+<p>Bring Service Down</p>
+<p>Bring Service Up</p>
+</div>
+
+<div id="footer">
+<p><center>Made with care by webconf</center></p>
+</div>
+</body>
+</html>
diff --git a/www/static/alpine.gif b/www/static/alpine.gif
new file mode 100644
index 0000000..2605afe
--- /dev/null
+++ b/www/static/alpine.gif
Binary files differ
diff --git a/www/static/alpine.png b/www/static/alpine.png
new file mode 100644
index 0000000..11802e0
--- /dev/null
+++ b/www/static/alpine.png
Binary files differ
diff --git a/www/static/arctic-back.png b/www/static/arctic-back.png
new file mode 100644
index 0000000..0719f28
--- /dev/null
+++ b/www/static/arctic-back.png
Binary files differ
diff --git a/www/static/arctic-upleft.png b/www/static/arctic-upleft.png
new file mode 100644
index 0000000..9b6c926
--- /dev/null
+++ b/www/static/arctic-upleft.png
Binary files differ
diff --git a/www/static/greypx.gif b/www/static/greypx.gif
new file mode 100644
index 0000000..b13cb90
--- /dev/null
+++ b/www/static/greypx.gif
Binary files differ
diff --git a/www/static/selected.gif b/www/static/selected.gif
new file mode 100644
index 0000000..b2c2951
--- /dev/null
+++ b/www/static/selected.gif
Binary files differ
diff --git a/www/static/submenu.css b/www/static/submenu.css
new file mode 100644
index 0000000..b759020
--- /dev/null
+++ b/www/static/submenu.css
@@ -0,0 +1,52 @@
+/*----------------------------------------------------------------------------------
+ * Submenu ( Horizontal Tabs)
+ *----------------------------------------------------------------------------------
+ */
+#submenu {
+ width: 75%;
+ font-size: 90%;
+// font-weight: Bold;
+// display: inline;
+ float: right;
+ margin: 0em 2em 0em 0em;
+ background: #fff url(/static/greypx.gif) repeat-x bottom left;
+ }
+
+#submenu p {
+ display: inline;
+ color: #22c;
+ font-size: 161%;
+ font-weight: light;
+ margin: 2em 0em -2em 0em;
+ }
+
+#submenu ul {
+ list-style: none;
+ display: inline;
+ margin: 0em 1em 0em 1em;
+ padding: 0em 3em .6em 2em;
+ float: right;
+ }
+
+#submenu li {
+ display: inline;
+ border: 1px solid #ccc;
+ padding: .5em 1em .5em 1em;
+ margin: 0em 0em 0em 0em;
+ border-bottom: none;
+ background: #f8f8f8;
+ background: transparent url("/static/unselected.gif") repeat-x top left;
+ }
+
+#submenu #selected {
+ // background: #fff url("/static/selected.gif") repeat-x top left;
+ background: #fff;
+ }
+
+#submenu a {
+ }
+
+#submenu a:hover {
+ color: #c22;
+ }
+
diff --git a/www/static/unselected.gif b/www/static/unselected.gif
new file mode 100644
index 0000000..f26d430
--- /dev/null
+++ b/www/static/unselected.gif
Binary files differ
diff --git a/www/static/webconf-purple.css b/www/static/webconf-purple.css
new file mode 100644
index 0000000..57538cd
--- /dev/null
+++ b/www/static/webconf-purple.css
@@ -0,0 +1,313 @@
+/* webconf.css - For Alpine webconf
+ */
+
+/* Default definitions */
+body {
+ background: url(arctic-back.png) repeat-y 20% 0;
+ // background-color: #DAE0EE;
+ font-family: Geneva, Arial, Helvetica, San-Serif;
+ margin: 0em;
+ padding: 0em;
+ // padding: 1em 0em 0em 1em;
+ font-size: 80%;
+ }
+
+p {
+ margin: 0em 0em 0em 0em;
+ }
+
+a {
+ text-decoration: none;
+ color: #333;
+ }
+
+a:link {
+ }
+
+a:visited {
+ }
+
+a:hover {
+ // font-weight: bold;
+ // text-decoration: underline;
+ }
+
+a:active {
+ }
+/*----------------------------------------------------------------------------------
+ * Header
+ *----------------------------------------------------------------------------------
+ */
+#head {
+ padding: 1em 1em 1em 0em;
+ background: #dde url(alpine.png) no-repeat 50% 0;
+ color: #333;
+ // margin: -1em 0em 0em 0em;
+ padding: 1em;
+ }
+
+#head h1 {
+ font-size: 300%;
+ color: #226;
+ font-family: Palatino, Times-Roman, Serif;
+ font-weight: bold;
+ margin: 0em 0em 0em 0em;
+ //float: left;
+ width: 49%;
+ }
+
+#head h2 {
+ margin: 0em 0em 0em 0em;
+ font-size: 100%;
+ font-weight: light;
+ float: right;
+ width: 30%;
+ }
+
+#head h3 {
+ float: right;
+ font-size: 90%;
+ color: #222;
+ }
+
+#head p {
+ // border: 1px inset #ccc;
+ // border-top: 1px solid #ccc;
+ // border-bottom: 1px solid #ccc;
+ }
+
+/*----------------------------------------------------------------------------------
+ * Left (Main) Menu
+ *----------------------------------------------------------------------------------
+ */
+#mainmenu {
+ display: block;
+ background-color: #dde;
+ width: 20%;
+ float: left;
+ margin: 2em 0em 0em 0em;
+ padding: 0em -1em 0em 1em;
+ }
+
+#mainmenu p {
+ background-color: #448;
+ color: #fc4;
+ padding: 0em 0em 0em 1em;
+ font-size: 110%;
+ font-weight: bold;
+ // margin: 0em 0em 0em 0em;
+ }
+
+#mainmenu ul {
+ padding: 0em 0em 0em 2em;
+ margin: 0;
+ list-style-type: none;
+ }
+#mainmenu li {
+ background-color: inherit;
+ }
+
+#mainmenu a:hover {
+ display: block;
+ background-color: #eec;
+ }
+
+#mainmenu #selected {
+ // border: 1px solid #666;
+ //border-right: 1px solid #ffd;
+ background-color: #ffe;
+ }
+
+
+/*----------------------------------------------------------------------------------
+ * Submenu ( Horizontal Tabs)
+ *----------------------------------------------------------------------------------
+ */
+#submenu {
+ // display: none;
+ width: 80%;
+ // clear: both;
+ display: inline;
+ float: right;
+ margin: 0em 0em 0em 0em;
+ background-color: #dde;
+ }
+
+#submenu p {
+ display: none;
+ }
+
+#submenu ul {
+ float: left;
+ list-style: none;
+ display: inline;
+ margin: 0 1em 0 1em;
+ padding: 0 1em 0 1em;
+ }
+
+#submenu li {
+ display: inline;
+ background-color: #ccd;
+ border: 1px solid #666;
+ border-bottom: none;
+ }
+
+#submenu #selected {
+ border: 1px solid #666;
+ background: #ffe;
+ border-bottom: none;
+ // 2px solid #ffd;
+ // font-weight: bold;
+ }
+
+#submenu a {
+ padding: .1em .2em;
+ }
+
+#submenu a:hover {
+ display: inline;
+ background: #eec;
+ padding: 0em .2em;
+ }
+
+/*----------------------------------------------------------------------------------
+ * Content
+ *----------------------------------------------------------------------------------
+ */
+#content {
+ float:left;
+ width: 70%;
+ color: #333;
+ // background-color: #DAE0EE;
+ background: url(arctic-upleft.png) no-repeat;
+ padding: 2em 0em 3em 3em;
+ }
+
+#content h1 {
+ font-size: 120%;
+ // width: 100%;
+ background-color: #eec;
+ border-bottom: 2px solid #aa9;
+ margin: 1em 0em 1em 0em;
+ clear: both;
+ }
+
+#content h2 {
+ font-size: 100%;
+ // width: 100%;
+ background-color: #eec;
+ border-bottom: 1px solid #aa9;
+ // margin: 1em 1em 0em 1em;
+ clear: both;
+ }
+
+#content p {
+ // margin: 1em 1em 0em 1em;
+ clear: both;
+ padding: .5em 0em .5em 0em;
+ }
+
+#content p.error {
+ // border: 2px solid #c00;
+ // margin: 1em 2em;
+ // padding: 1em 0em;
+ font-size: 120%;
+ font-weight: bold;
+ // background-color: #faa;
+ color: #c00;
+ }
+
+#content p.warn {
+ // border: 2px solid #cc0;
+ // margin: 1em 2em;
+ // padding: 1em;
+ background-color: #ffa;
+ }
+
+#content p.ok {
+ font-size: 120%;
+ font-weight: bold;
+ // border: 2px solid #0c0;
+ // margin: 1em 2em;
+ // padding: 1em 0em;
+ color: #090;
+ }
+
+
+#content input, textarea {
+ float: left;
+ font-size: 90%;
+ // font-family: courier-new, courier, sans;
+ background-color: #eec;
+ margin-top: -0.3em;
+ }
+
+
+#content input.error {
+ background-color: #faa;
+ // border: 2px solid #c00;
+ // margin: .5em 1em .5em 0em;
+ }
+
+#content input.button {
+ font-family: Geneva, Arial, Helvetica, Serif;
+ font-size: 100%;
+ font-weight: bold;
+ color: #228;
+ }
+
+#content dt {
+ font-size: 110%;
+ font-weight: bold;
+ color: #228;
+ margin: .7em 1em .5em 0em;
+ float: left;
+ border-top: 1px dotted #ccc;
+ border-bottom: 1px dotted #ccc;
+ width: 20%;
+ clear: both;
+ }
+
+#content a {
+ text-decoration: underline;
+ color: #449;
+ }
+
+#content form {
+ padding: 1em 0em;
+ }
+
+#content dl {
+ margin: 0em;
+ }
+
+#content dd {
+ color: #c00;
+ float: left;
+ margin: 1em 0em .5em 0em;
+ // font-size: 90%;
+ font-weight: light;
+ // margin: .5em 1em .5em 0em;
+ // border-top: 1px dotted #ccc;
+ // border-bottom: 1px dotted #ccc;
+ }
+
+
+
+/*----------------------------------------------------------------------------------
+ * footer
+ *----------------------------------------------------------------------------------
+ */
+
+#footer {
+ float: right;
+ clear: both;
+ text-align: center;
+ font-size: 80%;
+ width: 70%;
+ color: #666;
+ margin: 1em 3em 1em 2em;
+ padding: 1em 0em 1em;
+ border-top: 1px dotted #ccc;
+ border-bottom: 1px dotted #ccc;
+ }
diff --git a/www/static/webconf.css b/www/static/webconf.css
new file mode 100644
index 0000000..2c09494
--- /dev/null
+++ b/www/static/webconf.css
@@ -0,0 +1,367 @@
+/* webconf.css - For Alpine webconf
+ */
+
+/* Default definitions */
+body {
+ background: #fff url("/static/alpine.gif") no-repeat top center;
+ font-family: Geneva, Arial, Helvetica, San-Serif;
+ margin: 0em;
+ padding: 0em;
+ font-size: small;
+ }
+
+p {
+ margin: 0em 0em 0em 0em;
+ }
+
+a {
+ text-decoration: none;
+ color: #333;
+ }
+
+a:link { color: #226;
+ }
+
+a:visited {
+ color: #226;
+ }
+
+a:hover {
+ color: #44C;
+ }
+
+a:active {
+ }
+
+pre.code {
+ border: 1px solid #666;
+ background: #f8f8f8;
+ padding: .5em;
+}
+/*----------------------------------------------------------------------------------
+ * Header
+ *----------------------------------------------------------------------------------
+ */
+#head {
+ padding: 1em 1em 1em 0em;
+ color: #333;
+ // margin: -1em 0em 0em 0em;
+ padding: 1em;
+ }
+
+#head h1 {
+ font-size: 259%;
+ color: #33A;
+ // font-family: Palatino, Times-Roman, Serif;
+ font-weight: bold;
+ margin: 0em 0em 0em 0em;
+ //float: left;
+ // width: 49%;
+ }
+
+#head h2 {
+ margin: 0em 0em 0em 0em;
+ font-size: 100%;
+ font-weight: light;
+ float: right;
+ width: 30%;
+ }
+
+#head h3 {
+ float: right;
+ font-size: 90%;
+ color: #222;
+ }
+
+#head p {
+ // border: 1px inset #ccc;
+ // border-top: 1px solid #ccc;
+ // border-bottom: 1px solid #ccc;
+ }
+
+/*----------------------------------------------------------------------------------
+ * Left (Main) Menu
+ *----------------------------------------------------------------------------------
+ */
+#mainmenu {
+ display: block;
+ background: #eee;
+ // border: 1px #222 solid;
+ width: 19%;
+ float: left;
+ margin: 5em 0em 0em 1em;
+ padding: 0em 0em 0em .5em;
+ // border-right: 1px #ccc solid;
+ }
+
+#mainmenu h3 {
+ // background-color: #eee;
+ color: #448;
+ padding: .25em 0em 0em .25em;
+ font-weight: normal;
+ font-size: 138%;
+ margin: 0em 0em .1em 0em;
+ }
+
+#mainmenu ul {
+ padding: .2em 0em .6em 1em;
+ margin: 0;
+ list-style-type: none;
+ }
+#mainmenu li {
+ background-color: inherit;
+ padding: .1em 0em .1em 1em;
+ }
+
+#mainmenu a:hover {
+ display: block;
+// background: transparent url("/static/unselected.gif") repeat top left;
+// background-color: #f8f8f8;
+// color: #669;
+ text-decoration: underline;
+ }
+
+#mainmenu #selected {
+ background-color: #fff;
+ // border: 1px solid #666;
+ //border-right: 1px solid #ffd;
+ }
+
+
+/*----------------------------------------------------------------------------------
+ * Submenu ( Horizontal Tabs)
+ *----------------------------------------------------------------------------------
+ */
+#submenu {
+ width: 75%;
+ margin: 0 1em 0 0;
+// font-weight: Bold;
+ background: transparent url(/static/greypx.gif) repeat-x bottom left;
+ float: right;
+ }
+
+#submenu p {
+ display: none;
+ }
+
+#submenu h2 {
+ font-weight: normal;
+ font-size: 161%;
+ color: #228;
+ float: left;
+ margin: 0em;
+ margin-top: .2em;
+ }
+
+#submenu ul {
+ margin: 0em 2em 0em 0em;
+ list-style: none;
+ padding: 0em 2em 0em 2em;
+ float: right;
+ background: transparent;
+ }
+
+#submenu li {
+ // display: block;
+ // float: left;
+ // border: 1px solid #666;
+ // padding: .5em 1em .6em 1em;
+ // margin: 0em .5em 0em .5em;
+ // border-bottom: none;
+ color: #33A;
+ background: transparent url("/static/unselected.gif") repeat-x top left;
+ float: left;
+ margin: 0;
+ padding: 0;
+ }
+
+#submenu #selected {
+ // background: #fff url("/static/selected.gif") repeat-x top left;
+ background: #fff;
+ display: block;
+ float: left;
+ margin: 0em;
+ padding: .75em 1em;
+ border: 1px solid #ccc;
+ border-bottom: none;
+ }
+
+
+#submenu a {
+ display: block;
+ float: left;
+ margin: 0em;
+ padding: .75em 1em;
+ border: 1px solid #ccc;
+ border-bottom: none;
+ // background: #eee;
+ }
+
+#submenu a:hover {
+ // color: #226;
+// background: #eee;
+ text-decoration: underline;
+ }
+
+/*----------------------------------------------------------------------------------
+ * Content
+ *----------------------------------------------------------------------------------
+ */
+#content {
+ display: block;
+ float:left;
+ width: 70%;
+ color: #333;
+ padding: 2em 0em 3em 3em;
+ // border-left: #ccc 1px solid;
+ }
+
+#content h1 {
+ font-size: 120%;
+ // width: 100%;
+ background-color: #eee;
+ border-bottom: 2px solid #aaa;
+ margin: 1em 0em 1em 0em;
+ clear: both;
+ }
+
+#content h2 {
+ font-size: 100%;
+ // width: 100%;
+ background-color: #eee;
+ border-bottom: 1px solid #aaa;
+ // margin: 1em 1em 0em 1em;
+ clear: both;
+ }
+
+#content h3 {
+ font-size: 100%;
+ // width: 100%;
+ background-color: #fff;
+ border-bottom: 0px solid #eee;
+ margin: 0em 0em 0em 0em;
+ clear: both;
+ }
+
+#content p {
+ // margin: 1em 1em 0em 1em;
+ clear: both;
+ padding: .5em 0em .5em 0em;
+ }
+
+#content p.error {
+ // border: 2px solid #c00;
+ // margin: 1em 2em;
+ // padding: 1em 0em;
+ font-size: 120%;
+ font-weight: bold;
+ background-color: #eee;
+ color: #f44;
+ }
+
+#content p.warn {
+ // border: 2px solid #cc0;
+ // margin: 1em 2em;
+ // padding: 1em;
+ background-color: #cc4;
+ }
+
+#content p.ok {
+ font-size: 120%;
+ font-weight: bold;
+ // border: 2px solid #0c0;
+ // margin: 1em 2em;
+ // padding: 1em 0em;
+ color: #090;
+ }
+
+
+#content input, textarea {
+ float: left;
+ background-color: #eee;
+ margin-top: -0.3em;
+ }
+
+
+#content input.error {
+ /* yellow snow - bad */
+ background-color: #ffa;
+ }
+
+#content input.button {
+ font-family: Geneva, Arial, Helvetica, Serif;
+ font-size: 100%;
+ font-weight: bold;
+ color: #228;
+ }
+
+#content dt {
+ font-size: 110%;
+ // font-weight: bold;
+ color: #228;
+ margin: .7em 1em .5em 0em;
+ float: left;
+ border-top: 1px dotted #ccf;
+ border-bottom: 1px dotted #ccf;
+ // background-color: #eef;
+ width: 20%;
+ clear: both;
+ }
+
+#content th {
+ font-weight: inherit;
+ color: #228;
+ background: #eef;
+ }
+
+#content a {
+ text-decoration: underline;
+ color: #449;
+ }
+
+#content form {
+ padding: 1em 0em;
+ }
+
+#content dl {
+ margin: 0em;
+ }
+
+#content dd {
+ color: #444;
+ float: left;
+ margin: 1em 0em .5em 0em;
+ // font-size: 90%;
+ font-weight: light;
+ // margin: .5em 1em .5em 0em;
+ // border-top: 1px dotted #ccc;
+ // border-bottom: 1px dotted #ccc;
+ }
+
+#content .grey {
+ background-color: #eee;
+ }
+
+
+#content form {
+ display: inline;
+ }
+
+
+/*----------------------------------------------------------------------------------
+ * footer
+ *----------------------------------------------------------------------------------
+ */
+
+#footer {
+ float: right;
+ clear: both;
+ text-align: center;
+ font-size: 80%;
+ width: 70%;
+ color: #666;
+ margin: 1em 3em 1em 2em;
+ padding: 1em 0em 1em;
+ border-top: 1px dotted #ccc;
+ border-bottom: 1px dotted #ccc;
+ }