design is now object-oriented; added unrolling to static HTML r1-1-unrolling
authorTimm S. Mueller <tmueller@neoscientists.org>
Sat, 10 Mar 2007 21:28:27 +0100
changeset 12999b3008f0c77
parent 128 1d086a9f1613
child 130 e9cbf8e0cf26
design is now object-oriented; added unrolling to static HTML
Makefile
cgi-bin/loona.cgi
cgi-bin/loona.lua
cgi-bin/loona/editable.lua
cgi-bin/tek/web/markup.lua
etc/config.lua.sample
extensions/login.lua
extensions/search.lua
htdocs/index.lua
     1.1 --- a/Makefile	Thu Mar 08 00:51:31 2007 +0100
     1.2 +++ b/Makefile	Sat Mar 10 21:28:27 2007 +0100
     1.3 @@ -46,7 +46,7 @@
     1.4  	if test ! -f $(CONTENTDIR)/current_$(LANGUAGE)/copyright; then cp COPYRIGHT $(CONTENTDIR)/current_$(LANGUAGE)/copyright; fi
     1.5  	if test ! -f $(CONTENTDIR)/current_$(LANGUAGE)/markup; then cp MARKUP $(CONTENTDIR)/current_$(LANGUAGE)/markup; fi
     1.6  	if test ! -f $(CONTENTDIR)/current_$(LANGUAGE)/login; then echo -e 'INCLUDE(login)' >> $(CONTENTDIR)/current_$(LANGUAGE)/login; fi
     1.7 -	if test ! -f $(CONTENTDIR)/current_$(LANGUAGE)/.sections; then echo -e '[1] = { name = "home" },\n[2] = { name = "copyright" },\n[3] = { name = "markup" },\n[4] = { name = "login" },' >> $(CONTENTDIR)/current_$(LANGUAGE)/.sections; fi
     1.8 +	if test ! -f $(CONTENTDIR)/current_$(LANGUAGE)/.sections; then echo -e '[1] = { name = "home" },\n[2] = { name = "copyright" },\n[3] = { name = "markup" },\n[4] = { name = "login", dynamic = { main = true } },' >> $(CONTENTDIR)/current_$(LANGUAGE)/.sections; fi
     1.9  
    1.10  
    1.11  permissions:
    1.12 @@ -57,8 +57,8 @@
    1.13  	chown -R $(WWWUSER) $(SESSIONDIR)
    1.14  	chown -R $(WWWUSER) $(CONTENTDIR)
    1.15  	find . -name CVS -type d | xargs -r chmod g+rw
    1.16 -	chown $(WWWUSER):$(GROUP) $(ETCDIR)/passwd.lua
    1.17 -	chmod 460 $(ETCDIR)/passwd.lua
    1.18 +	-chown $(WWWUSER):$(GROUP) $(ETCDIR)/passwd.lua
    1.19 +	-chmod 460 $(ETCDIR)/passwd.lua
    1.20  
    1.21  
    1.22  all: modules setup permissions
     2.1 --- a/cgi-bin/loona.cgi	Thu Mar 08 00:51:31 2007 +0100
     2.2 +++ b/cgi-bin/loona.cgi	Sat Mar 10 21:28:27 2007 +0100
     2.3 @@ -7,29 +7,25 @@
     2.4  --
     2.5  
     2.6  require "tek"
     2.7 -require "tek.cgi.document"
     2.8 -require "tek.web.include"
     2.9 +local loona = require "loona"
    2.10  local cgi = require "tek.cgi"
    2.11  local web = require "tek.web"
    2.12 -out = web.out
    2.13 +local handler = require "tek.cgi.document".Handler
    2.14 +local include = require "tek.web.include".load
    2.15  
    2.16 -local dh = cgi.document.Handler
    2.17 -if dh then
    2.18 +if handler then
    2.19  
    2.20 -	-- parse and execute the addressed script
    2.21 +	-- Load and execute the addressed handler script
    2.22  	
    2.23 -	local errcode, errtext, errdetail, errtrace = tek.catch(function()
    2.24 -		local parsed, msg = web.include.load(io.open(dh), "out", dh)
    2.25 -		if not parsed then
    2.26 -			tek.throw(1001, "HTML/Lua parsing failed", msg)
    2.27 -		end
    2.28 -		parsed()
    2.29 +	local loonainst
    2.30 +	local errcode, text, detail, trace = tek.catch(function()
    2.31 +		loonainst = loona:new():execute()
    2.32  	end)
    2.33  	
    2.34  	if errcode ~= 0 then
    2.35  		web.discard()
    2.36  		web.setheader "Content-Type: text/html; charset=utf-8\n\n"
    2.37 -		out([[
    2.38 +		tek.web.out([[
    2.39  			<?xml version="1.0"?>
    2.40  			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    2.41  			<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    2.42 @@ -39,10 +35,11 @@
    2.43  				</head>
    2.44  				<body>
    2.45  					<h2>Application error ]] .. (errcode or "").. [[</h2>
    2.46 -					<pre>]] .. cgi.encodeform(errtext) .. [[</pre>]])
    2.47 -		if not loona or loona.config and loona.config.debug then
    2.48 -			out([[	<pre>]] .. cgi.encodeform(errdetail) .. [[</pre>
    2.49 -					<pre>]] .. cgi.encodeform(errtrace) .. [[</pre>
    2.50 +					<pre>]] .. cgi.encodeform(text) .. [[</pre>]])
    2.51 +		if loonainst and loonainst.authuser then
    2.52 +			tek.web.out([[
    2.53 +					<pre>]] .. cgi.encodeform(detail) .. [[</pre>
    2.54 +					<pre>]] .. cgi.encodeform(trace) .. [[</pre>
    2.55  				</body>
    2.56  			</html>]])
    2.57  		end
    2.58 @@ -50,8 +47,8 @@
    2.59  
    2.60  else
    2.61  
    2.62 -	out("Content-Type: text/plain\n\n")
    2.63 -	out("Error: No handler to serve a document at the addressed location")
    2.64 +	tek.web.out("Content-Type: text/plain\n\n")
    2.65 +	tek.web.out("No handler to serve a document at the addressed location")
    2.66  
    2.67  end
    2.68  
     3.1 --- a/cgi-bin/loona.lua	Thu Mar 08 00:51:31 2007 +0100
     3.2 +++ b/cgi-bin/loona.lua	Sat Mar 10 21:28:27 2007 +0100
     3.3 @@ -34,85 +34,12 @@
     3.4  local open, remove, rename, getenv, time =
     3.5  	io.open, os.remove, os.rename, os.getenv, os.time
     3.6  
     3.7 -local sectionfname, langs, docname
     3.8 -
     3.9  
    3.10  module "loona"
    3.11  
    3.12  
    3.13 -_VERSION = 2
    3.14 -_REVISION = 2
    3.15 -
    3.16 -
    3.17 -out = tek.web.out
    3.18 -setheader = tek.web.setheader
    3.19 -session = cgi.session
    3.20 -request = cgi.request
    3.21 -args = request.args
    3.22 -encodeform = cgi.encodeform
    3.23 -loadhtml = tek.web.include.load
    3.24 -source = tek.source
    3.25 -domarkup = tek.web.markup.main
    3.26 -expire = tek.util.expire
    3.27 -
    3.28 -
    3.29 -local function dbmsg(msg, detail)
    3.30 -	return (msg and detail and config.debug) and
    3.31 -		("%s : %s"):format(msg, detail) or msg
    3.32 -end
    3.33 -
    3.34 -
    3.35 -local function checkprofilename(c)
    3.36 -	assert(c:match("^%w+$") and c ~= "current",
    3.37 -		dbmsg("Invalid profile name", c))
    3.38 -	return c
    3.39 -end
    3.40 -
    3.41 -
    3.42 -local function checkbodyname(s)
    3.43 -	s = s or "main"
    3.44 -	assert(s:match("^[%w_]*%w+[%w_]*$"), dbmsg("Invalid body name", s))
    3.45 -	return s
    3.46 -end
    3.47 -
    3.48 -
    3.49 -local function deletedir(dst)
    3.50 -	for e in tek.util.readdir(dst) do
    3.51 - 		local success, msg = remove(dst .. "/" .. e)
    3.52 -		assert(success, dbmsg("Error removing entry in profile", msg))
    3.53 -	end
    3.54 -	return remove(dst)
    3.55 -end
    3.56 -
    3.57 -
    3.58 -local function copyprofile(contentdir, lang, srcprofile, newprofile)
    3.59 -	local src = ("%s/%s_%s"):format(contentdir, srcprofile, lang)
    3.60 -	assert(posix.stat(src, "mode") == "directory",
    3.61 -		dbmsg("Not a directory", src))
    3.62 -	local dst = ("%s/%s_%s"):format(contentdir, newprofile, lang)
    3.63 -	local success, msg = posix.mkdir(dst)
    3.64 -	assert(success, dbmsg("Error creating profile directory", msg))
    3.65 -	for e in tek.util.readdir(src) do
    3.66 -		local ext = e:match("^[^.].*%.([^.]*)$")
    3.67 -		if ext ~= "LOCK" then
    3.68 -			local f = src .. "/" .. e
    3.69 -			if posix.stat(f, "mode") == "file" then
    3.70 -				success, msg = tek.copyfile(f, dst .. "/" .. e)
    3.71 -				assert(success, dbmsg("Error copying file", msg))
    3.72 -			end
    3.73 -		end
    3.74 -	end
    3.75 -end
    3.76 -
    3.77 -
    3.78 -local function publishprofile(contentdir, lang, profile)
    3.79 -	local newpath = ("%s/current_%s"):format(contentdir, lang)
    3.80 -	local tmppath = newpath .. "." .. session.name
    3.81 -	local success, msg = posix.symlink(profile .. "_" .. lang, tmppath)
    3.82 -	assert(success, dbmsg("Cannot create symlink", msg))
    3.83 -	success, msg = rename(tmppath, newpath)
    3.84 -	assert(success, dbmsg("Cannot put symlink in place", msg))
    3.85 -end
    3.86 +_VERSION = 3
    3.87 +_REVISION = 0
    3.88  
    3.89  
    3.90  local function lookupname(tab, val)
    3.91 @@ -126,18 +53,108 @@
    3.92  end
    3.93  
    3.94  
    3.95 ---	Index sections, determine accessibility and visibility in menu
    3.96 +local atom = { }
    3.97  
    3.98 -local function indexsections(s)
    3.99 +function atom:new(o)
   3.100 +	o = o or { }
   3.101 +	setmetatable(o, self)
   3.102 +	self.__index = self
   3.103 +	return o
   3.104 +end
   3.105 +
   3.106 +local loona = atom:new(getfenv())
   3.107 +
   3.108 +
   3.109 +function loona:dbmsg(msg, detail)
   3.110 + 	return (msg and detail and self.authuser) and
   3.111 + 		("%s : %s"):format(msg, detail) or msg
   3.112 +end
   3.113 +
   3.114 +
   3.115 +function loona:checkprofilename(n)
   3.116 +	assert(n:match("^%w+$") and n ~= "current",
   3.117 +		self:dbmsg("Invalid profile name", n))
   3.118 +	return n
   3.119 +end
   3.120 +
   3.121 +
   3.122 +function loona:checkbodyname(s)
   3.123 +	s = s or "main"
   3.124 +	assert(s:match("^[%w_]*%w+[%w_]*$"), self:dbmsg("Invalid body name", s))
   3.125 +	return s
   3.126 +end
   3.127 +
   3.128 +
   3.129 +function loona:deleteprofile(p, lang)
   3.130 +	p = self.config.contentdir .. "/" .. p .. "_" .. (lang or self.lang)
   3.131 +	for e in tek.util.readdir(p) do
   3.132 + 		local success, msg = remove(p .. "/" .. e)
   3.133 +		assert(success, self:dbmsg("Error removing entry in profile", msg))
   3.134 +	end
   3.135 +	return remove(p)
   3.136 +end
   3.137 +
   3.138 +
   3.139 +function loona:copyprofile(newprofile, srcprofile, lang)
   3.140 +	srcprofile = srcprofile or self.profile
   3.141 +	lang = lang or self.lang
   3.142 +	local contentdir = self.config.contentdir
   3.143 +	local src = ("%s/%s_%s"):format(contentdir, srcprofile or self.profile, lang)
   3.144 +	assert(posix.stat(src, "mode") == "directory",
   3.145 +		self:dbmsg("Not a directory", src))
   3.146 +	local dst = ("%s/%s_%s"):format(contentdir, newprofile, lang)
   3.147 +	local success, msg = posix.mkdir(dst)
   3.148 +	assert(success, self:dbmsg("Error creating profile directory", msg))
   3.149 +	for e in tek.util.readdir(src) do
   3.150 +		local ext = e:match("^[^.].*%.([^.]*)$")
   3.151 +		if ext ~= "LOCK" then
   3.152 +			local f = src .. "/" .. e
   3.153 +			if posix.stat(f, "mode") == "file" then
   3.154 +				success, msg = tek.copyfile(f, dst .. "/" .. e)
   3.155 +				assert(success, self:dbmsg("Error copying file", msg))
   3.156 +			end
   3.157 +		end
   3.158 +	end
   3.159 +end
   3.160 +
   3.161 +
   3.162 +function loona:publishprofile(profile, lang)
   3.163 +	lang = lang or self.lang
   3.164 +	local contentdir = self.config.contentdir
   3.165 +	local newpath = ("%s/current_%s"):format(contentdir, lang)
   3.166 +	local tmppath = newpath .. "." .. self.session.name
   3.167 +	local success, msg = posix.symlink(profile .. "_" .. lang, tmppath)
   3.168 +	assert(success, self:dbmsg("Cannot create symlink", msg))
   3.169 +	success, msg = rename(tmppath, newpath)
   3.170 +	assert(success, self:dbmsg("Cannot put symlink in place", msg))
   3.171 +	
   3.172 +	-- Unroll site to static HTML
   3.173 +	
   3.174 +	self:recursesections(self.sections, function(self, s, e, path)
   3.175 +		path = path and path .. "/" .. e.name or e.name
   3.176 + 		loona:dumphtml { requestpath = path }
   3.177 +		return path
   3.178 +	end)
   3.179 +end
   3.180 +
   3.181 +
   3.182 +function loona:recursesections(s, func, ...)
   3.183  	for _, e in ipairs(s) do
   3.184 +		local udata = { func(self, s, e, unpack(arg)) }
   3.185  		if e.subs then
   3.186 -			indexsections(e.subs)
   3.187 +			self:recursesections(e.subs, func, unpack(udata))
   3.188  		end
   3.189 -		e.notvalid = (not secure and e.secure) or 
   3.190 -			(not authuser and e.secret) or nil
   3.191 -		e.notvisible = e.notvalid or not authuser and e.hidden or nil
   3.192 +	end
   3.193 +end
   3.194 +
   3.195 +
   3.196 +function loona:indexsections()
   3.197 +	self:recursesections(self.sections, function(self, s, e)
   3.198 +		e.notvalid = (not self.secure and e.secure) or 
   3.199 +			(not self.authuser and e.secret) or nil
   3.200 +		e.notvisible = e.notvalid or not self.authuser and e.hidden or nil
   3.201  		s[e.name] = e
   3.202 -	end
   3.203 +	end)
   3.204  end
   3.205  
   3.206  
   3.207 @@ -145,11 +162,12 @@
   3.208  --	the last valid element in the path. additionally returns the table of
   3.209  --	the last section path element (or the default section)
   3.210  
   3.211 -local function getsection(section, authuser, path, default)
   3.212 -	local tab = { { entries = sections, name = default } }
   3.213 -	local ss = sections
   3.214 +function loona:getsection(path)
   3.215 +	local default = not self.authuser and self.config.defname
   3.216 +	local tab = { { entries = self.sections, name = default } }
   3.217 +	local ss = self.sections
   3.218  	local sectionpath
   3.219 -	(path or default):gsub("(%w+)/?", function(a)
   3.220 +	(path or ""):gsub("(%w+)/?", function(a)
   3.221  		if ss then
   3.222  			local s = ss[a]
   3.223  			if s and not s.notvalid then
   3.224 @@ -164,8 +182,8 @@
   3.225  			end
   3.226  		end
   3.227  	end)
   3.228 -	if not section and not sectionpath then
   3.229 -		sectionpath = sections[default]
   3.230 +	if not self.section and not sectionpath then
   3.231 +		sectionpath = self.sections[default]
   3.232  		if sectionpath then
   3.233  			table.insert(tab, { entries = sectionpath.subs })
   3.234  		end
   3.235 @@ -174,9 +192,9 @@
   3.236  end
   3.237  
   3.238  
   3.239 -local function getpath(menus, delimiter)
   3.240 +function loona:getpath(delimiter)
   3.241  	local t = { }
   3.242 -	for _, menu in ipairs(menus) do
   3.243 +	for _, menu in ipairs(self.submenus) do
   3.244  		if menu.name then
   3.245  			table.insert(t, menu.name)
   3.246  		end
   3.247 @@ -185,34 +203,15 @@
   3.248  end
   3.249  
   3.250  
   3.251 ---	Descending into the sections table alongside the current path,
   3.252 ---	return the filename to include, defaulting to its parent value
   3.253 ---	(or the default specified)
   3.254 -
   3.255 -local function getsectionfile(menus, path, ext, default)
   3.256 -	local t, val = { }
   3.257 -	for _, menu in ipairs(menus) do
   3.258 -		if menu.entries and menu.entries[menu.name] then
   3.259 -			table.insert(t, menu.name)
   3.260 -			local fn = table.concat(t, "_") .. ext
   3.261 -			if posix.stat(path .. "/" .. fn, "mode") == "file" then
   3.262 -				val = fn
   3.263 -			end
   3.264 -		end
   3.265 -	end
   3.266 -	return val or default
   3.267 -end
   3.268 -
   3.269 -
   3.270 -local function deletesection(dir, fname)
   3.271 -	local fullname = dir .. "/" .. fname
   3.272 +function loona:deletesection(fname, all_bodies)
   3.273 +	local fullname = self.contentdir .. "/" .. fname
   3.274  	local success, msg = remove(fullname)
   3.275 -	if success then
   3.276 +	if all_bodies then
   3.277  		local pat = "^" .. 
   3.278  			fname:gsub("%^%$%(%)%%%.%[%]%*%+%-%?", "%%%1") .. "%..*$"
   3.279 -		for e in tek.util.readdir(dir) do
   3.280 +		for e in tek.util.readdir(self.contentdir) do
   3.281  			if e:match(pat) then
   3.282 -				remove(dir .. "/" .. e)
   3.283 +				remove(self.contentdir .. "/" .. e)
   3.284  			end
   3.285  		end
   3.286  	end
   3.287 @@ -220,9 +219,8 @@
   3.288  end
   3.289  
   3.290  
   3.291 ---	Add element to path
   3.292 -
   3.293 -local function addtopath(tab, path, e)
   3.294 +function loona:addpath(path, e)
   3.295 +	local tab = self.sections
   3.296  	path:gsub("(%w+)/?", function(a)
   3.297  		if tab then
   3.298  			local s = tab[a]
   3.299 @@ -238,13 +236,13 @@
   3.300  			end
   3.301  		end
   3.302  	end)
   3.303 +	return e
   3.304  end
   3.305  
   3.306  
   3.307 ---	Remove element from path
   3.308 -
   3.309 -local function rmpath(tab, path)
   3.310 +function loona:rmpath(path)
   3.311  	local parent
   3.312 +	local tab = self.sections
   3.313  	path:gsub("(%w+)/?", function(a)
   3.314  		if tab then
   3.315  			local idx = lookupname(tab, a)
   3.316 @@ -266,201 +264,168 @@
   3.317  end
   3.318  
   3.319  
   3.320 --------------------------------------------------------------------------------
   3.321 -
   3.322 -
   3.323 ---	Produce page title
   3.324 -
   3.325 -function title()
   3.326 -	return section and (section.title or section.label or section.name) or ""
   3.327 +function loona:checkpath(path)
   3.328 +	if path ~= "index" then -- "index" is reserved
   3.329 +		local res, idx
   3.330 +		local tab = self.sections
   3.331 +		path:gsub("(%w+)/?", function(a)
   3.332 +			if tab then
   3.333 +				local i = lookupname(tab, a)
   3.334 +				if i then
   3.335 +					res, idx = tab, i
   3.336 +					tab = tab[i].subs
   3.337 +				else
   3.338 +					res, idx = nil, nil
   3.339 +				end
   3.340 +			end
   3.341 +		end)
   3.342 +		return res, idx
   3.343 +	end
   3.344  end
   3.345  
   3.346  
   3.347 ---	Create proxy for on-demand loading of locale strings
   3.348 -
   3.349 -locale = { }
   3.350 -local locmt = { }
   3.351 -locmt.__index = function(_, key)
   3.352 -	for _, l in ipairs(langs) do
   3.353 -		locmt.__locale = source(config.localedir .. "/" .. l)
   3.354 -		if locmt.__locale then
   3.355 -			break
   3.356 -		end
   3.357 -	end
   3.358 -	locmt.__index = function(tab, key)
   3.359 -		return locmt.__locale[key] or key
   3.360 -	end
   3.361 -	return locmt.__locale[key] or key
   3.362 -end
   3.363 -setmetatable(locale, locmt)
   3.364 -
   3.365 -
   3.366 ---	Find element in path
   3.367 -
   3.368 -function checkpath(tab, path)
   3.369 -	local res, idx
   3.370 -	path:gsub("(%w+)/?", function(a)
   3.371 -		if tab then
   3.372 -			local i = lookupname(tab, a)
   3.373 -			if i then
   3.374 -				res, idx = tab, i
   3.375 -				tab = tab[i].subs
   3.376 -			else
   3.377 -				res, idx = nil, nil
   3.378 -			end
   3.379 -		end
   3.380 -	end)
   3.381 -	return res, idx
   3.382 +function loona:title()
   3.383 +	return self.section and (self.section.title or self.section.label or
   3.384 +		self.section.name) or ""
   3.385  end
   3.386  
   3.387  
   3.388  --	Run a site function snippet, with full error recovery
   3.389  --	(also recovers from errors in error handling function)
   3.390  
   3.391 -function dosnippet(config, func, errfunc)
   3.392 +function loona:dosnippet(func, errfunc)
   3.393  	local ret = { tek.catch(func) }
   3.394  	if ret[1] == 0 or (errfunc and tek.catch(errfunc) == 0) then
   3.395  		return unpack(ret)
   3.396  	end
   3.397 -	out("<h2>Error</h2>")
   3.398 -	out("<h3>" .. cgi.encodeform(ret[2] or "") .. "</h3>")
   3.399 -	if config.debug then
   3.400 +	self:out("<h2>Error</h2>")
   3.401 +	self:out("<h3>" .. self:encodeform(ret[2] or "") .. "</h3>")
   3.402 +	if self.authuser then
   3.403  		if type(ret[3]) == "string" then
   3.404 -			out("<p>" .. cgi.encodeform(ret[3]) .. "</p>")
   3.405 +			self:out("<p>" .. self:encodeform(ret[3]) .. "</p>")
   3.406  		end
   3.407 -		if ret[4] and config.debug then
   3.408 -			out("<pre>" .. cgi.encodeform(ret[4]) .. "</pre>")
   3.409 +		if ret[4] then
   3.410 +			self:out("<pre>" .. self:encodeform(ret[4]) .. "</pre>")
   3.411  		end
   3.412  	end
   3.413  end	
   3.414  
   3.415  
   3.416 -function lockfile(newfile)
   3.417 -	return not session and true or 
   3.418 -		posix.symlink(session.filename, newfile .. ".LOCK")
   3.419 +function loona:lockfile(file)
   3.420 +	return not self.session and true or 
   3.421 +		posix.symlink(self.session.filename, file .. ".LOCK")
   3.422  end
   3.423  
   3.424  
   3.425 -function unlockfile(dstfile)
   3.426 -	return not session and true or remove(dstfile .. ".LOCK")
   3.427 +function loona:unlockfile(file)
   3.428 +	return not self.session and true or remove(file .. ".LOCK")
   3.429  end
   3.430  
   3.431  
   3.432 -function saveindex()
   3.433 -	local tempname = sectionfname .. "." .. session.name
   3.434 +function loona:saveindex()
   3.435 +	local tempname = self.indexfname .. "." .. self.session.name
   3.436  	local f, msg = open(tempname, "wb")
   3.437 -	assert(f, dbmsg("Error opening section file for writing", msg))
   3.438 -	tek.dump(sections, function(...)
   3.439 +	assert(f, self:dbmsg("Error opening section file for writing", msg))
   3.440 +	tek.dump(self.sections, function(...)
   3.441  		f:write(unpack(arg))
   3.442  	end)
   3.443  	f:close()
   3.444 -	local success, msg = rename(tempname, sectionfname)
   3.445 -	assert(success, dbmsg("Error renaming section file", msg))
   3.446 +	local success, msg = rename(tempname, self.indexfname)
   3.447 +	assert(success, self:dbmsg("Error renaming section file", msg))
   3.448  end
   3.449  
   3.450  
   3.451 -function savesection(dir, fname, content)
   3.452 -	fname = dir .. "/" .. fname
   3.453 +function loona:savebody(fname, content)
   3.454 +	fname = self.contentdir .. "/" .. fname
   3.455  	local f, msg = open(fname, "wb")
   3.456 -	assert(f, dbmsg("Could not open file for writing", msg))
   3.457 +	assert(f, self:dbmsg("Could not open file for writing", msg))
   3.458  	f:write(content or "")
   3.459  	f:close()
   3.460  end
   3.461  
   3.462  
   3.463 ---	Run extension in a controlled environment
   3.464 +function loona:runboxed(func, envitems, ...)
   3.465 +	local fenv = {
   3.466 + 		arg = arg,
   3.467 + 		loona = self
   3.468 + 	}
   3.469 + 	if envitems then
   3.470 +	 	for k, v in pairs(envitems) do
   3.471 + 			fenv[k] = v
   3.472 + 		end
   3.473 + 	end
   3.474 + 	setmetatable(fenv, { __index = boxed_G }) -- boxed global environment
   3.475 +	setfenv(func, fenv)
   3.476 +	return func()
   3.477 +end
   3.478  
   3.479 -function include(fname, ...)
   3.480 -	assert(not fname:match("%W"), dbmsg("Invalid include name", fname))
   3.481 -	local fname2 = ("%s/%s.lua"):format(config.extdir, fname)
   3.482 +
   3.483 +function loona:include(fname, ...)
   3.484 +	assert(not fname:match("%W"), self:dbmsg("Invalid include name", fname))
   3.485 +	local fname2 = ("%s/%s.lua"):format(self.config.extdir, fname)
   3.486  	local f, msg = open(fname2)
   3.487 -	assert(f, dbmsg("Cannot open file", msg))
   3.488 -	local parsed, msg = loadhtml(f, "loona.out", fname2)
   3.489 -	assert(parsed, dbmsg("Syntax error", msg))
   3.490 - 	local fenv = {
   3.491 - 		arg = arg,
   3.492 - 		loona = {
   3.493 - 			out = out,
   3.494 - 			setheader = setheader,
   3.495 -			hidden = hidden,
   3.496 -			link = link,
   3.497 -			elink = elink,
   3.498 -			plink = plink,
   3.499 -			href = href,
   3.500 -			checkpath = checkpath,
   3.501 -			authuser = authuser,
   3.502 -			document = document,
   3.503 -			contentdir = contentdir,
   3.504 -			profile = profile,
   3.505 -			pubprofile = pubprofile,
   3.506 -			lang = lang,
   3.507 -			locale = locale,
   3.508 -			secure = secure,
   3.509 -			config = config,
   3.510 -			sectionpath = sectionpath,
   3.511 -			sections = sections,
   3.512 -			session = session,
   3.513 -			loginfailed = loginfailed
   3.514 -		}
   3.515 -	}
   3.516 - 	setmetatable(fenv, { __index = boxed_G }) -- boxed global environment
   3.517 -	setfenv(parsed, fenv)
   3.518 -	return parsed()
   3.519 +	assert(f, self:dbmsg("Cannot open file", msg))
   3.520 +	local parsed, msg = self:loadhtml(f, "loona:out", fname2)
   3.521 +	assert(parsed, self:dbmsg("Syntax error", msg))
   3.522 +	return self:runboxed(parsed)
   3.523  end
   3.524  
   3.525  
   3.526  --	produce link target, propagate lang, profile, session
   3.527  
   3.528 -function href(section, ...)
   3.529 -	local target = section and docname .. "/" .. section or docname
   3.530 -	if session or profile ~= pubprofile then
   3.531 +function loona:href(section, ...)
   3.532 +	local target = self:getdocname(section)
   3.533 +-- 	if self.session or self.profile ~= self.pubprofile then
   3.534 +	if self.session then
   3.535  		return tek.web.gethref(target, { "profile", "session", "lang", 
   3.536  			unpack(arg) })
   3.537  	end
   3.538  	return tek.web.gethref(target, { "lang", unpack(arg) })
   3.539  end
   3.540  
   3.541 -local function ilink(target, text, extra)
   3.542 +function loona:ilink(target, text, extra)
   3.543  	return ('<a href="%s"%s>%s</a>'):format(target, extra or "", text)
   3.544  end
   3.545  
   3.546  --	internal link, propagation of lang, profile, session
   3.547  
   3.548 -function link(section, text, ...)
   3.549 -	return ilink(href(section, unpack(arg)), text or section)
   3.550 +function loona:link(section, text, ...)
   3.551 +	return self:ilink(self:href(section, unpack(arg)), text or section)
   3.552  end
   3.553  
   3.554  --	external link (opens in a new window), no argument propagation
   3.555  
   3.556 -function elink(target, text)
   3.557 -	return ilink(target, text or target, not config.extlinksamewindow and
   3.558 +function loona:elink(target, text)
   3.559 +	return self:ilink(target, text or target, not self.config.extlinksamewindow and
   3.560  		' class="extlink" onclick="void(window.open(this.href, \'\', \'\')); return false;"')
   3.561  end
   3.562  
   3.563  --	plain link, no argument propagation
   3.564  
   3.565 -function plink(section, text, ...)
   3.566 -	return ilink(tek.web.gethref(section, arg), text or section)
   3.567 +function loona:plink(section, text, ...)
   3.568 +	return self:ilink(tek.web.gethref(section, arg), text or section)
   3.569  end
   3.570  
   3.571  --	user interface link, propagation of lang, profile, session
   3.572  
   3.573 -function uilink(section, text, ...)
   3.574 -	return ilink(href(section, unpack(arg)), text or section)
   3.575 +function loona:uilink(section, text, ...)
   3.576 +	return self:ilink(self:href(section, unpack(arg)), text or section)
   3.577  end
   3.578  
   3.579  --	produce a hidden input value in forms
   3.580  
   3.581 -function hidden(name, value)
   3.582 +function loona:hidden(name, value)
   3.583  	return not value and "" or
   3.584  		('<input type="hidden" name="%s" value="%s" />'):format(name, value)
   3.585  end
   3.586  
   3.587  
   3.588 -function getprofiles(contentdir, lang)
   3.589 +function loona:getprofiles(lang)
   3.590 +	lang = lang or self.lang
   3.591 +	local dir = self.config.contentdir
   3.592  	local t = { }
   3.593 -	for f in tek.util.readdir(contentdir) do
   3.594 -		if posix.lstat(contentdir .. "/" .. f, "mode") == "directory" then
   3.595 +	for f in tek.util.readdir(dir) do
   3.596 +		if posix.lstat(dir .. "/" .. f, "mode") == "directory" then
   3.597  			local e = f:match("^(%w+)_" .. lang .. "$")
   3.598   			if e then
   3.599  	 			t[e] = e
   3.600 @@ -473,12 +438,11 @@
   3.601  
   3.602  --	Functions to produce a hierarchical navigation menu
   3.603  
   3.604 -local newent = { name = "new", label = "[+]",
   3.605 -	action="actionnew=true" }
   3.606 +local newent = { name = "new", label = "[+]", action="actionnew=true" }
   3.607  
   3.608 -local function rmenu(level, linkf, path)
   3.609 -	local sub = (authuser and level == #submenus + 1) and
   3.610 -		{ name = "new", entries = { }} or submenus[level]
   3.611 +function loona:rmenu(level, linkf, path)
   3.612 +	local sub = (self.authuser and level == #self.submenus + 1) and
   3.613 +		{ name = "new", entries = { }} or self.submenus[level]
   3.614   	if sub and sub.entries then
   3.615  		local visible = { }
   3.616  		for _, e in ipairs(sub.entries) do
   3.617 @@ -486,204 +450,188 @@
   3.618  				table.insert(visible, e)
   3.619  			end
   3.620  		end
   3.621 -		if authuser then
   3.622 +		if self.authuser then
   3.623  			table.insert(visible, newent)
   3.624  		end
   3.625  		if #visible > 0 then
   3.626 -			out('<ul id="menulevel' .. level .. '">\n')
   3.627 +			self:out('<ul id="menulevel' .. level .. '">\n')
   3.628  			for _, e in ipairs(visible) do
   3.629 -				local label = encodeform(e.label or e.name)
   3.630 +				local label = self:encodeform(e.label or e.name)
   3.631  				local newpath = path and path .. "/" .. e.name or e.name
   3.632  				local active = (e.name == sub.name)
   3.633 -				out('<li>\n')
   3.634 -				linkf(newpath, label, active, e.action)
   3.635 +				self:out('<li>\n')
   3.636 +				linkf(self, newpath, label, active, e.action)
   3.637  				if active then
   3.638 -					rmenu(level + 1, linkf, newpath)
   3.639 +					self:rmenu(level + 1, linkf, newpath)
   3.640  				end
   3.641 -				out('</li>\n')
   3.642 +				self:out('</li>\n')
   3.643  			end
   3.644 -			out('</ul>\n')
   3.645 +			self:out('</ul>\n')
   3.646  		end
   3.647  	end
   3.648  end
   3.649  
   3.650 -local function menulink(path, label, active, ...)
   3.651 -	out(('<a %shref="%s">%s</a>\n'):format(active and 'class="active" ' or "",
   3.652 -		href(path, unpack(arg)), label))
   3.653 +function loona:menulink(path, label, active, ...)
   3.654 +	self:out(('<a %shref="%s">%s</a>\n'):format(active and 'class="active" ' or "",
   3.655 +		self:href(path, unpack(arg)), label))
   3.656  end
   3.657  
   3.658 -function menu(level, linkf)
   3.659 -	rmenu(level or 1, linkf or menulink)
   3.660 +function loona:menu(level, linkf)
   3.661 +	self:rmenu(level or 1, linkf or menulink)
   3.662  end
   3.663  
   3.664  
   3.665 --- Init
   3.666 -
   3.667 -local function init()
   3.668 +function loona:init()
   3.669  	
   3.670  	-- get list of languages, in order of preference
   3.671  	
   3.672 -	langs = { args.lang and args.lang:match("^%w+$") }
   3.673 -	if config.browserlang then
   3.674 +	self.langs = { self.args.lang and self.args.lang:match("^%w+$") }
   3.675 +	if self.config.browserlang then
   3.676  		local s = getenv("HTTP_ACCEPT_LANGUAGE")
   3.677  		while s do
   3.678  			local l, r = s:match("^([%w.=]+)[,;](.*)$")
   3.679  			l = l or s
   3.680  			s = r
   3.681  			if l:match("^%w+$") then
   3.682 -				table.insert(langs, l)
   3.683 +				table.insert(self.langs, l)
   3.684  			end
   3.685  		end
   3.686  	end
   3.687 -	table.insert(langs, config.deflang)
   3.688 +	table.insert(self.langs, self.config.deflang)
   3.689  	
   3.690  	-- get list of possible profiles
   3.691  	
   3.692  	local profiles = { }
   3.693 -	for e in tek.util.readdir(config.contentdir) do
   3.694 +	for e in tek.util.readdir(self.config.contentdir) do
   3.695  		profiles[e] = e
   3.696  	end
   3.697  	
   3.698  	-- get pubprofile
   3.699  	
   3.700 -	for _, lang in ipairs(langs) do
   3.701 -		local p = posix.readlink(config.contentdir .. "/current_" .. lang)
   3.702 +	for _, lang in ipairs(self.langs) do
   3.703 +		local p = posix.readlink(self.config.contentdir .. "/current_" .. lang)
   3.704  		p = p and p:match("^(%w+)_" .. lang .. "$")
   3.705  		if p then
   3.706 -			pubprofile = p
   3.707 +			self.pubprofile = p
   3.708  			break
   3.709  		end
   3.710  	end
   3.711  	
   3.712  	-- get profile
   3.713  	
   3.714 -	local checkprofile = authuser and args.profile or pubprofile or "default"
   3.715 -	for _, l in ipairs(langs) do
   3.716 -		if profiles[checkprofile .. "_" .. l] then
   3.717 -			profile = checkprofile
   3.718 -			lang = l
   3.719 +	local checkprofile = self.authuser and self.args.profile or self.pubprofile or "default"
   3.720 +	for _, lang in ipairs(self.langs) do
   3.721 +		if profiles[checkprofile .. "_" .. lang] then
   3.722 +			self.profile = checkprofile
   3.723 +			self.lang = lang
   3.724  			break
   3.725  		end
   3.726  	end
   3.727  	
   3.728 -	assert(profile and lang, "Invalid profile or language")
   3.729 +	assert(self.profile and self.lang, "Invalid profile or language")
   3.730  	
   3.731  	
   3.732 -	-- Write back language and profile
   3.733 +	-- write back language and profile
   3.734  
   3.735 -	args.lang = lang ~= config.deflang and lang or nil
   3.736 -	args.profile = profile
   3.737 +	self.args.lang = self.lang ~= self.config.deflang and self.lang or nil
   3.738 +	self.args.profile = self.profile
   3.739  	
   3.740  	
   3.741  	-- determine content directory pathname and section filename
   3.742  	
   3.743 -	contentdir = ("%s/%s_%s"):format(config.contentdir, profile, lang)
   3.744 - 	sectionfname = contentdir .. "/.sections"
   3.745 +	self.contentdir = ("%s/%s_%s"):format(self.config.contentdir, self.profile, self.lang)
   3.746 + 	self.indexfname = self.contentdir .. "/.sections"
   3.747  	
   3.748  	-- load sections
   3.749  	
   3.750 - 	sections = source(sectionfname)
   3.751 + 	self.sections = tek.source(self.indexfname)
   3.752  	
   3.753  	-- index sections, determine visibility in menu
   3.754  	
   3.755 -	indexsections(sections)
   3.756 +	self:indexsections()
   3.757  	
   3.758 -	-- decompose section path, produce a stack of sections
   3.759 +	-- decompose request path, produce a stack of sections
   3.760  	
   3.761 -	submenus, section = getsection(section, authuser, 
   3.762 -		cgi.document.VirtualPath or "", not authuser and config.defname)
   3.763 +	self.submenus, self.section = self:getsection(self.requestpath)
   3.764  
   3.765  	-- handle redirects if not logged on
   3.766  	
   3.767 -	if not authuser and section and section.redirect then
   3.768 -		submenus, section = getsection(section, authuser, 
   3.769 -			section.redirect, not authuser and config.defname)
   3.770 +	if not self.authuser and self.section and self.section.redirect then
   3.771 +		self.submenus, self.section = self:getsection(self.section.redirect)
   3.772  	end
   3.773  			
   3.774  	-- section path and document name (refined)
   3.775  	
   3.776 -	sectionpath = getpath(submenus)
   3.777 -	sectionname = getpath(submenus, "_")
   3.778 +	self.sectionpath = self:getpath()
   3.779 +	self.sectionname = self:getpath("_")
   3.780  
   3.781  end
   3.782  
   3.783  
   3.784 ---	Handle state modifications (create/save/delete, profile management)
   3.785 +function loona:handlechanges()
   3.786 +	
   3.787 +	local save
   3.788  
   3.789 -local function handlestate()
   3.790 -
   3.791 -	if args.editkey == "main" then
   3.792 +	if self.args.editkey == "main" then
   3.793  		
   3.794  		-- In main editable section:
   3.795  		
   3.796 -		local save
   3.797 -		
   3.798 -		if args.actioncreate then
   3.799 +		if self.args.actioncreate then
   3.800 +			
   3.801  			-- Create new section
   3.802 -			local editname = args.editname:lower()
   3.803 +			
   3.804 +			local editname = self.args.editname:lower()
   3.805  			assert(not editname:match("%W"),
   3.806  				dbmsg("Invalid section name", editname))
   3.807  			if not (section and (section.subs or section)[editname]) then
   3.808  				local newpath = 
   3.809 -					(sectionpath and (sectionpath .. "/")) .. editname
   3.810 -				addtopath(sections, newpath, { name = editname,
   3.811 -					label = args.editlabel ~= "" and args.editlabel or nil,
   3.812 -					title = args.edittitle ~= "" and args.edittitle or nil,
   3.813 -					redirect = args.editredirect ~= "" and args.editredirect or nil,
   3.814 -					hidden = args.editvisibility and true,
   3.815 -					secret = args.editsecrecy and true,
   3.816 -					secure = args.editsecure and true,
   3.817 -					creator = authuser,
   3.818 +					(self.sectionpath and (self.sectionpath .. "/")) .. editname
   3.819 +				local s = self:addpath(newpath, { name = editname,
   3.820 +					label = self.args.editlabel ~= "" and self.args.editlabel or nil,
   3.821 +					title = self.args.edittitle ~= "" and self.args.edittitle or nil,
   3.822 +					redirect = self.args.editredirect ~= "" and self.args.editredirect or nil,
   3.823 +					hidden = self.args.editvisibility and true,
   3.824 +					secret = self.args.editsecrecy and true,
   3.825 +					secure = self.args.editsecure and true,
   3.826 +					creator = self.authuser,
   3.827  					creationdate = time() })
   3.828  				save = true
   3.829  			end
   3.830  		
   3.831 -		elseif args.actionsave then
   3.832 +		elseif self.args.actionsave then
   3.833 +			
   3.834  			-- Save section
   3.835 -			section.revisiondate = time()
   3.836 -			section.revisioner = authuser
   3.837 +			
   3.838 +			self.section.revisiondate = time()
   3.839 +			self.section.revisioner = self.authuser
   3.840 +			save = true
   3.841 + 		
   3.842 +		elseif self.args.actionsaveprops then
   3.843 +			
   3.844 +			-- Save properties
   3.845 +			
   3.846 +			self.section.hidden = self.args.editvisibility and true
   3.847 +			self.section.secret = self.args.editsecrecy and true
   3.848 +			self.section.secure = self.args.editsecure and true
   3.849 +			self.section.label = self.args.editlabel ~= "" and self.args.editlabel or nil
   3.850 +			self.section.title = self.args.edittitle ~= "" and self.args.edittitle or nil
   3.851 +			self.section.redirect =
   3.852 +				self.args.editredirect ~= "" and self.args.editredirect or nil
   3.853  			save = true
   3.854  		
   3.855 -		elseif args.actiondelete then
   3.856 -			-- Delete section
   3.857 -			if not args.actionconfirm then
   3.858 -				useralert = {
   3.859 -					text = profile == pubprofile and
   3.860 -						locale.ALERT_DELETE_SECTION_IN_PUBLISHED_PROFILE or
   3.861 -						locale.ALERT_DELETE_SECTION,
   3.862 -					confirm =
   3.863 -						'<input type="submit" name="actiondelete" value="' .. 
   3.864 -						locale.DELETE .. '" /> ' ..
   3.865 -						hidden("actionconfirm", "true")
   3.866 -				}
   3.867 -			else
   3.868 -				deletesection(contentdir, sectionname)
   3.869 -				rmpath(sections, sectionpath)
   3.870 -				save = true
   3.871 -			end
   3.872 -		
   3.873 -		elseif args.actionsaveprops then
   3.874 -			-- Save properties
   3.875 -			section.hidden = args.editvisibility and true
   3.876 -			section.secret = args.editsecrecy and true
   3.877 -			section.secure = args.editsecure and true
   3.878 -			section.label = args.editlabel ~= "" and args.editlabel or nil
   3.879 -			section.title = args.edittitle ~= "" and args.edittitle or nil
   3.880 -			section.redirect =
   3.881 -				args.editredirect ~= "" and args.editredirect or nil
   3.882 -			save = true
   3.883 -		
   3.884 -		elseif args.actionup then
   3.885 +		elseif self.args.actionup then
   3.886 +			
   3.887  			-- Move section up
   3.888 -			local t, i = checkpath(sections, sectionpath)
   3.889 +			
   3.890 +			local t, i = self:checkpath(self.sectionpath)
   3.891  			if t and i > 1 then
   3.892 -				if profile == pubprofile and not args.actionconfirm then
   3.893 +				if self.profile == self.pubprofile and not self.args.actionconfirm then
   3.894  					useralert = {
   3.895 -						text = locale.ALERT_MOVE_SECTION_IN_PUBLISHED_PROFILE,
   3.896 +						text = self.locale.ALERT_MOVE_SECTION_IN_PUBLISHED_PROFILE,
   3.897  						confirm =
   3.898  							'<input type="submit" name="actionup" value="' .. 
   3.899 -							locale.MOVE .. '" /> ' ..
   3.900 -							hidden("actionconfirm", "true")
   3.901 +							self.locale.MOVE .. '" /> ' ..
   3.902 +							self:hidden("actionconfirm", "true")
   3.903  					}
   3.904  				else
   3.905  					local item = table.remove(t, i)
   3.906 @@ -692,17 +640,19 @@
   3.907  				end
   3.908  			end
   3.909  		
   3.910 -		elseif args.actiondown then
   3.911 +		elseif self.args.actiondown then
   3.912 +			
   3.913  			-- Move section down
   3.914 -			local t, i = checkpath(sections, sectionpath)
   3.915 +			
   3.916 +			local t, i = self:checkpath(self.sectionpath)
   3.917  			if t and i < #t then
   3.918 -				if profile == pubprofile and not args.actionconfirm then
   3.919 +				if self.profile == self.pubprofile and not self.args.actionconfirm then
   3.920  					useralert = {
   3.921 -						text = locale.ALERT_MOVE_SECTION_IN_PUBLISHED_PROFILE,
   3.922 +						text = self.locale.ALERT_MOVE_SECTION_IN_PUBLISHED_PROFILE,
   3.923  						confirm =
   3.924  							'<input type="submit" name="actiondown" value="' .. 
   3.925 -							locale.MOVE .. '" /> ' ..
   3.926 -							hidden("actionconfirm", "true")
   3.927 +							self.locale.MOVE .. '" /> ' ..
   3.928 +							self:hidden("actionconfirm", "true")
   3.929  					}
   3.930  				else
   3.931  					local item = table.remove(t, i)
   3.932 @@ -711,177 +661,358 @@
   3.933  				end
   3.934  			end
   3.935  		
   3.936 -		elseif args.actioncreateprofile and args.createprofile then
   3.937 +		elseif self.args.actioncreateprofile and self.args.createprofile then
   3.938 +			
   3.939  			-- Create profile
   3.940 -			local c = checkprofilename(args.createprofile:lower())
   3.941 +			
   3.942 +			local c = self:checkprofilename(self.args.createprofile:lower())
   3.943  			if c == profile then
   3.944 -				useralert = { text = locale.ALERT_CANNOT_COPY_PROFILE_TO_SELF }
   3.945 +				useralert = { text = self.locale.ALERT_CANNOT_COPY_PROFILE_TO_SELF }
   3.946  			else
   3.947 -				local profiles = getprofiles(config.contentdir, lang)
   3.948 -				if profiles[c] and not args.actionconfirm then
   3.949 +				local profiles = self:getprofiles()
   3.950 +				if profiles[c] and not self.args.actionconfirm then
   3.951  					useralert = {
   3.952 -						text = c == pubprofile and 
   3.953 -							locale.ALERT_OVERWRITE_PUBLISHED_PROFILE or
   3.954 -							locale.ALERT_OVERWRITE_EXISTING_PROFILE,
   3.955 +						text = c == self.pubprofile and 
   3.956 +							self.locale.ALERT_OVERWRITE_PUBLISHED_PROFILE or
   3.957 +							self.locale.ALERT_OVERWRITE_EXISTING_PROFILE,
   3.958  						confirm =
   3.959  							'<input type="submit" name="actioncreateprofile" value="' .. 
   3.960 -							locale.OVERWRITE .. '" /> ' ..
   3.961 -							hidden("actionconfirm", "true") .. 
   3.962 -							hidden("createprofile", c)
   3.963 +							self.locale.OVERWRITE .. '" /> ' ..
   3.964 +							self:hidden("actionconfirm", "true") .. 
   3.965 +							self:hidden("createprofile", c)
   3.966  					}
   3.967  				else
   3.968  					if profiles[c] then
   3.969 -						deletedir(config.contentdir .. "/" .. c .. "_" .. lang)
   3.970 +						self:deleteprofile(c)
   3.971  					end
   3.972 -					copyprofile(config.contentdir, lang, profile, c)
   3.973 +					self:copyprofile(c)
   3.974  				end
   3.975  			end
   3.976  		
   3.977 -		elseif args.actiondeleteprofile and args.deleteprofile then
   3.978 +		elseif self.args.actiondeleteprofile and self.args.deleteprofile then
   3.979 +			
   3.980  			-- Delete profile
   3.981 -			local c = checkprofilename(args.deleteprofile:lower())
   3.982 -			assert(c ~= pubprofile, dbmsg("Cannot delete published profile", c))
   3.983 -			if args.actionconfirm then
   3.984 -				deletedir(config.contentdir .. "/" .. c .. "_" .. lang)
   3.985 -				profile = nil
   3.986 -				args.profile = nil
   3.987 -				init()
   3.988 +			
   3.989 +			local c = self:checkprofilename(self.args.deleteprofile:lower())
   3.990 +			assert(c ~= self.pubprofile, self:dbmsg("Cannot delete published profile", c))
   3.991 +			if self.args.actionconfirm then
   3.992 +				self:deleteprofile(c)
   3.993 +				self.profile = nil
   3.994 +				self.args.profile = nil
   3.995 +				self:init()
   3.996  				save = true
   3.997  			else
   3.998  				useralert = { 
   3.999 -					text = locale.ALERT_DELETE_PROFILE,
  3.1000 +					text = self.locale.ALERT_DELETE_PROFILE,
  3.1001  					confirm = 
  3.1002  						'<input type="submit" name="actiondeleteprofile" value="' .. 
  3.1003 -						locale.DELETE .. '" /> ' ..
  3.1004 -						hidden("actionconfirm", "true") ..
  3.1005 -						hidden("deleteprofile", c)
  3.1006 +						self.locale.DELETE .. '" /> ' ..
  3.1007 +						self:hidden("actionconfirm", "true") ..
  3.1008 +						self:hidden("deleteprofile", c)
  3.1009  				}
  3.1010  			end
  3.1011  		
  3.1012 -		elseif args.actionchangeprofile and args.changeprofile then
  3.1013 +		elseif self.args.actionchangeprofile and self.args.changeprofile then
  3.1014 +			
  3.1015  			-- Change profile
  3.1016 -			local c = checkprofilename(args.changeprofile:lower())
  3.1017 -			profile = c
  3.1018 -			args.profile = c
  3.1019 +			
  3.1020 +			local c = self:checkprofilename(self.args.changeprofile:lower())
  3.1021 +			self.profile = c
  3.1022 +			self.args.profile = c
  3.1023  			save = true
  3.1024  		
  3.1025 -		elseif args.actionpublishprofile and args.publishprofile then
  3.1026 +		elseif self.args.actionpublishprofile and self.args.publishprofile then
  3.1027 +			
  3.1028  			-- Publish profile
  3.1029 -			local c = checkprofilename(args.publishprofile:lower())
  3.1030 -			if c ~= _publicprofile then
  3.1031 -				if args.actionconfirm then
  3.1032 -					publishprofile(config.contentdir, lang, c)
  3.1033 +			
  3.1034 +			local c = self:checkprofilename(self.args.publishprofile:lower())
  3.1035 +			if c ~= self.publicprofile then
  3.1036 +				if self.args.actionconfirm then
  3.1037 +					self:publishprofile(c)
  3.1038  					save = true
  3.1039  				else
  3.1040  					useralert = {
  3.1041 -						text = locale.ALERT_PUBLISH_PROFILE,
  3.1042 +						text = self.locale.ALERT_PUBLISH_PROFILE,
  3.1043  						confirm =
  3.1044  							'<input type="submit" name="actionpublishprofile" value="' ..
  3.1045 -							locale.PUBLISH .. '" /> ' ..
  3.1046 -							hidden("actionconfirm", "true") ..
  3.1047 -							hidden("publishprofile", c)
  3.1048 +							self.locale.PUBLISH .. '" /> ' ..
  3.1049 +							self:hidden("actionconfirm", "true") ..
  3.1050 +							self:hidden("publishprofile", c)
  3.1051  					}
  3.1052  				end
  3.1053  			end
  3.1054  		end
  3.1055  		
  3.1056 -		if save then
  3.1057 -			saveindex()
  3.1058 -			init()
  3.1059 -		end
  3.1060 +	end
  3.1061  	
  3.1062 -	elseif args.editkey and checkbodyname(args.editkey) then
  3.1063 -		if args.actiondelete then
  3.1064 -			-- Delete section in secondary editable body:
  3.1065 -			deletesection(contentdir, sectionname .. "." .. args.editkey)
  3.1066 +	if self.args.actiondelete then
  3.1067 +		
  3.1068 +		-- Delete section
  3.1069 +		
  3.1070 +		if not self.args.actionconfirm then
  3.1071 +			useralert = {
  3.1072 +				text = self.profile == self.pubprofile and
  3.1073 +					self.locale.ALERT_DELETE_SECTION_IN_PUBLISHED_PROFILE or
  3.1074 +					self.locale.ALERT_DELETE_SECTION,
  3.1075 +				confirm =
  3.1076 +					'<input type="submit" name="actiondelete" value="' .. 
  3.1077 +					self.locale.DELETE .. '" /> ' ..
  3.1078 +					self:hidden("actionconfirm", "true")
  3.1079 +			}
  3.1080 +		else
  3.1081 +			local key = self.args.editkey
  3.1082 +			if key == "main" and not self.section.subs then
  3.1083 +				self:deletesection(self.sectionname, true) -- all bodies
  3.1084 +				self:rmpath(self.sectionpath) -- and node
  3.1085 +			else
  3.1086 +				local ext = (key == "main" and "") or "." .. key
  3.1087 +				self:deletesection(self.sectionname .. ext) -- only text
  3.1088 +				if self.section.dynamic then
  3.1089 +					self.section.dynamic[key] = nil
  3.1090 +				end
  3.1091 +			end
  3.1092 +			save = true
  3.1093  		end
  3.1094  	end
  3.1095 +		
  3.1096 +	if save then
  3.1097 +		self:saveindex()
  3.1098 +		self:init()
  3.1099 +	end
  3.1100 +	
  3.1101  end
  3.1102  
  3.1103  
  3.1104 ---	Load/create configuration
  3.1105 +function loona:encodeform(s)
  3.1106 +	return cgi.encodeform(s)
  3.1107 +end
  3.1108  
  3.1109 -config = source("../etc/config.lua") or { }
  3.1110  
  3.1111 -config.defname = config.defname or "home"
  3.1112 -config.deflang = config.deflang or "en"
  3.1113 -config.sessionmaxage = config.sessionmaxage or 600
  3.1114 -config.secureport = config.secureport or 443
  3.1115 +function loona:loadhtml(src, outfunc, chunkname)
  3.1116 +	return tek.web.include.load(src, outfunc, chunkname)
  3.1117 +end
  3.1118  
  3.1119 ---	Check paths and make them absolute
  3.1120  
  3.1121 -config.passwdfile = posix.abspath(config.passwdfile or "../etc/passwd.lua")
  3.1122 -config.sessiondir = posix.abspath(config.sessiondir or "../var/sessions")
  3.1123 -config.extdir = posix.abspath(config.extdir or "../extensions")
  3.1124 -config.contentdir = posix.abspath(config.contentdir or "../content")
  3.1125 -config.localedir = posix.abspath(config.localedir or "../locale")
  3.1126 +function loona:domarkup(s)
  3.1127 +	return tek.web.markup.main(s)
  3.1128 +end
  3.1129  
  3.1130 ---	Manage login and establish session
  3.1131  
  3.1132 -session.init(config.sessiondir, args.session, config.sessionmaxage)
  3.1133 -if args.login then
  3.1134 -	if args.login == "false" then
  3.1135 -		session.delete()
  3.1136 -		session = nil
  3.1137 -	elseif args.password then
  3.1138 -		loginfailed = true
  3.1139 -		local pwddb = source(config.passwdfile)
  3.1140 -		local pwdentry = pwddb[args.login]
  3.1141 -		if pwdentry and pwdentry.password == args.password then
  3.1142 -			session.data.authuser = pwdentry.username
  3.1143 -			session.data.id = session.id
  3.1144 -			loginfailed = nil
  3.1145 +function loona:expire(dir, pat, maxage)
  3.1146 +	return tek.util.expire(dir, pat, maxage)
  3.1147 +end
  3.1148 +
  3.1149 +--
  3.1150 +--	Get pathname of an existing content file that
  3.1151 +--	the current path is determined by or defaults to
  3.1152 +--
  3.1153 +
  3.1154 +function loona:getsectionpath(bodyname, requestpath)
  3.1155 +	local ext = (not bodyname or bodyname == "main") and "" or "." .. bodyname
  3.1156 +	local t, path, section = { }
  3.1157 +	for _, menu in ipairs(self.submenus) do
  3.1158 +		if menu.entries and menu.entries[menu.name] then
  3.1159 +			table.insert(t, menu.name)
  3.1160 +			local fn = table.concat(t, "_")
  3.1161 +			if posix.stat(self.contentdir .. "/" .. fn .. ext, "mode") == "file" then
  3.1162 +				path, section = fn, menu
  3.1163 +			end
  3.1164  		end
  3.1165  	end
  3.1166 +	return path, ext, section
  3.1167  end
  3.1168  
  3.1169 -secure = request.Port == config.secureport
  3.1170 -authuser = session and session.data.authuser
  3.1171  
  3.1172 -if not authuser then
  3.1173 -	session = nil
  3.1174 -	args.session = nil
  3.1175 +function loona:body(name)
  3.1176 +	name = self:checkbodyname(name)
  3.1177 +	local path, ext = self:getsectionpath(name)
  3.1178 +	self:dosnippet(self.editable(name, path and path .. ext, self.sectionname .. ext))
  3.1179  end
  3.1180  
  3.1181  
  3.1182 ---	get lang, locale, profile, section
  3.1183 +function loona:new(o)
  3.1184  
  3.1185 -init()
  3.1186 -if authuser then
  3.1187 -	-- handle state modifications
  3.1188 -	handlestate()
  3.1189 +	local parsed, msg
  3.1190 +	
  3.1191 +	o = o or { }
  3.1192 +	o = atom.new(self, o)
  3.1193 +	
  3.1194 +	o.out = o.out or function(self, s) tek.web.out(s) end
  3.1195 +	o.setheader = o.setheader or function(self, s) tek.web.setheader(s) end
  3.1196 +	
  3.1197 +	-- Get configuration
  3.1198 +	
  3.1199 +	o.config = o.config or tek.source(o.conffile or "../etc/config.lua") or { }
  3.1200 +	o.config.defname = o.config.defname or "home"
  3.1201 +	o.config.deflang = o.config.deflang or "en"
  3.1202 +	o.config.sessionmaxage = o.config.sessionmaxage or 600
  3.1203 +	o.config.secureport = o.config.secureport or 443
  3.1204 +	o.config.passwdfile = posix.abspath(o.config.passwdfile or "../etc/passwd.lua")
  3.1205 +	o.config.sessiondir = posix.abspath(o.config.sessiondir or "../var/sessions")
  3.1206 +	o.config.extdir = posix.abspath(o.config.extdir or "../extensions")
  3.1207 +	o.config.contentdir = posix.abspath(o.config.contentdir or "../content")
  3.1208 +	o.config.localedir = posix.abspath(o.config.localedir or "../locale")
  3.1209 +	o.config.htdocsdir = posix.abspath(o.config.htdocsdir or "../htdocs")
  3.1210 +	
  3.1211 +	-- Create proxy for on-demand loading of locales
  3.1212 +	
  3.1213 +	o.locale = { }
  3.1214 +	local locmt = { }
  3.1215 +	locmt.__index = function(_, key)
  3.1216 +		for _, l in ipairs(o.langs) do
  3.1217 +			locmt.__locale = tek.source(o.config.localedir .. "/" .. l)
  3.1218 +			if locmt.__locale then
  3.1219 +				break
  3.1220 +			end
  3.1221 +		end
  3.1222 +		locmt.__index = function(tab, key)
  3.1223 +			return locmt.__locale[key] or key
  3.1224 +		end
  3.1225 +		return locmt.__locale[key] or key
  3.1226 +	end
  3.1227 +	setmetatable(o.locale, locmt)
  3.1228 +	
  3.1229 +	-- Get request, args, document, script name, request path
  3.1230 +	
  3.1231 +	o.request = o.request or cgi.request
  3.1232 +	o.args = o.args or cgi.request.args
  3.1233 +	o.session = o.session or cgi.session
  3.1234 + 	
  3.1235 + 	o.scriptpath = o.scriptpath or cgi.document.Path
  3.1236 +	o.requesthandler = o.requesthandler or cgi.document.Handler
  3.1237 + 	o.requestdocument = o.requestdocument or cgi.document.Name
  3.1238 +	o.requestpath = o.requestpath or cgi.document.VirtualPath
  3.1239 +
  3.1240 +	-- Manage login and establish session
  3.1241 +	
  3.1242 +	o.session.init(o.config.sessiondir, o.args.session, o.config.sessionmaxage)
  3.1243 +	if o.args.login then
  3.1244 +		if o.args.login == "false" then
  3.1245 +			o.session.delete()
  3.1246 +			o.session = nil
  3.1247 +		elseif o.args.password then
  3.1248 +			o.loginfailed = true
  3.1249 +			local pwddb = tek.source(o.config.passwdfile)
  3.1250 +			local pwdentry = pwddb[o.args.login]
  3.1251 +			if pwdentry and pwdentry.password == o.args.password then
  3.1252 +				o.session.data.authuser = pwdentry.username
  3.1253 +				o.session.data.id = o.session.id
  3.1254 +				o.loginfailed = nil
  3.1255 +			end
  3.1256 +		end
  3.1257 +	end
  3.1258 +	
  3.1259 +	o.secure = o.request.Port == o.config.secureport
  3.1260 +	o.authuser = o.session and o.session.data.authuser
  3.1261 +	
  3.1262 +	if o.nologin or not o.authuser then
  3.1263 +		o.authuser = nil
  3.1264 +		o.session = nil
  3.1265 +-- 		o.args.session = nil -- TODO?
  3.1266 +	end
  3.1267 +	
  3.1268 +	-- Get lang, locale, profile, section
  3.1269 +	
  3.1270 +	o:init()
  3.1271 +	if o.authuser then
  3.1272 +		o:handlechanges()
  3.1273 +	end
  3.1274 +	
  3.1275 +	-- Current document
  3.1276 +	
  3.1277 +	o.document = o.requestdocument .. "/" .. o.sectionpath
  3.1278 +	if o.authuser then
  3.1279 +		o.getdocname = function(self, path)
  3.1280 +			return path and self.requestdocument .. "/" .. path or self.requestdocument
  3.1281 +		end
  3.1282 +	else
  3.1283 +		o.getdocname = function(self, path)
  3.1284 +			path = path or self.config.defname
  3.1285 +			if self:isdynamic(path) then
  3.1286 +				return self.requestdocument .. "/" .. path
  3.1287 +			end
  3.1288 +			path = path == self.config.defname and "index" or path
  3.1289 +			return "/" .. path:gsub("/", "_") .. ".html"
  3.1290 +		end
  3.1291 +	end
  3.1292 +	
  3.1293 +	-- Create "editable section" function
  3.1294 +	
  3.1295 +	local func, msg = o:loadhtml(open("loona/editable.lua"),
  3.1296 +		"loona:out", "loona/editable.lua")
  3.1297 + 	assert(func, o:dbmsg("Syntax error", msg))
  3.1298 +	o.editable = o:runboxed(func)
  3.1299 +	
  3.1300 +	-- Save session state
  3.1301 +	
  3.1302 +	if o.session then
  3.1303 +		o.session.save()
  3.1304 +	end
  3.1305 +
  3.1306 +	return o
  3.1307  end
  3.1308  
  3.1309  
  3.1310 ---	current document
  3.1311 +function loona:execute(fname)
  3.1312 +	self:indexdynamic() -- TODO: this a solution?
  3.1313 +	fname = fname or self.requesthandler
  3.1314 +	local parsed, msg = self:loadhtml(open(fname), "loona:out", fname)
  3.1315 +	assert(parsed, self:dbmsg("HTML/Lua parsing failed", msg))
  3.1316 +	self:runboxed(parsed)
  3.1317 +	return self
  3.1318 +end
  3.1319  
  3.1320 -docname = cgi.document.Name
  3.1321 -document = docname .. "/" .. sectionpath
  3.1322  
  3.1323 +function loona:indexdynamic()
  3.1324 +	self:recursesections(self.sections, function(self, s, e, path, dynamic)
  3.1325 +		path = path and path .. "_" .. e.name or e.name
  3.1326 +		dynamic = dynamic or { }
  3.1327 +		for k in pairs(e.dynamic or { }) do
  3.1328 +			dynamic[k] = true
  3.1329 +		end
  3.1330 +		for k in pairs(dynamic) do
  3.1331 +			local ext = (k == "main" and "") or "." .. k
  3.1332 +			if posix.stat(self.contentdir .. "/" .. path .. ext, "mode") == "file" then
  3.1333 +				dynamic[k] = e.dynamic and e.dynamic[k]
  3.1334 +			end
  3.1335 +		end
  3.1336 +		local n = 0
  3.1337 +		for k in pairs(dynamic) do
  3.1338 +			n = n + 1
  3.1339 +		end
  3.1340 +		if n > 0 then
  3.1341 +			e.dynamic = { }
  3.1342 +			for k in pairs(dynamic) do
  3.1343 +				e.dynamic[k] = true
  3.1344 +			end
  3.1345 +		else
  3.1346 +			e.dynamic = nil
  3.1347 +		end
  3.1348 +		return path, dynamic
  3.1349 +	end)
  3.1350 +end
  3.1351  
  3.1352 ---	create section function
  3.1353  
  3.1354 -local func, msg = loadhtml(open("loona/editable.lua"),
  3.1355 -	"tek.web.out", "loona/editable.lua")
  3.1356 +function loona:isdynamic(path)
  3.1357 +	path = path or self.sectionpath
  3.1358 +	local t, i = self:checkpath(path)
  3.1359 +	return t and t[i].dynamic
  3.1360 +end
  3.1361  
  3.1362 -assert(func, dbmsg("Syntax error", msg))
  3.1363  
  3.1364 -local editable = func()
  3.1365 -
  3.1366 -function body(name)
  3.1367 -	name = checkbodyname(name)
  3.1368 -	if name == "main" then
  3.1369 -		dosnippet(config, editable("main", contentdir, sectionname, sectionname))
  3.1370 -	else
  3.1371 -		local ext = "." .. name
  3.1372 -		dosnippet(config, editable(name, contentdir,
  3.1373 -			getsectionfile(submenus, contentdir, ext), sectionname .. ext))
  3.1374 +function loona:dumphtml(o)
  3.1375 +	local outbuf = { }
  3.1376 +	o = o or { }
  3.1377 +	o.nologin = true
  3.1378 +	o.out = function(self, s) table.insert(outbuf, s) end
  3.1379 +	o.setheader = function(self, s) end
  3.1380 +	o = self:new(o):execute()
  3.1381 +	if not o:isdynamic() then
  3.1382 +		local path = o.sectionname
  3.1383 +		path = path == o.config.defname and "index" or path
  3.1384 +		local fname = o.config.htdocsdir .. "/" .. path .. ".html"
  3.1385 +		local fh = open(fname, "wb")
  3.1386 +		fh:write(unpack(outbuf))
  3.1387 +		fh:close()
  3.1388  	end
  3.1389  end
  3.1390 -
  3.1391 -
  3.1392 ---	write back session state
  3.1393 -
  3.1394 -if session then
  3.1395 -	session.save()
  3.1396 -end
     4.1 --- a/cgi-bin/loona/editable.lua	Thu Mar 08 00:51:31 2007 +0100
     4.2 +++ b/cgi-bin/loona/editable.lua	Sat Mar 10 21:28:27 2007 +0100
     4.3 @@ -1,5 +1,9 @@
     4.4  <%
     4.5  
     4.6 +local os = require "os"
     4.7 +local io = require "io"
     4.8 +local tek = require "tek"
     4.9 +
    4.10  local function loadcontent(contentdir, fname)
    4.11  	local c
    4.12  	if fname then
    4.13 @@ -28,7 +32,7 @@
    4.14  		end
    4.15  		local c, dynamic
    4.16  		c = loadcontent(contentdir, fname)
    4.17 -		c, dynamic = loona.domarkup(c)
    4.18 +		c, dynamic = loona:domarkup(c)
    4.19  		if not dynamic and loona.config.cachehtml == true then
    4.20  			f = io.open(htmlfname, "wb")
    4.21  			if f then
    4.22 @@ -43,12 +47,14 @@
    4.23  
    4.24  
    4.25  do
    4.26 -	return function(editkey, contentdir, fname, savename)
    4.27 +	return function(editkey, fname, savename)
    4.28 +		local contentdir = loona.contentdir
    4.29 +	
    4.30  		local function hiddenvars()%>
    4.31 -			<%=loona.hidden("lang", loona.args.lang)%>
    4.32 -			<%=loona.hidden("profile", loona.profile)%>
    4.33 -			<%=loona.hidden("session", loona.session.id)%>
    4.34 -			<%=loona.hidden("editkey", editkey)%>
    4.35 +			<%=loona:hidden("lang", loona.args.lang)%>
    4.36 +			<%=loona:hidden("profile", loona.profile)%>
    4.37 +			<%=loona:hidden("session", loona.session.id)%>
    4.38 +			<%=loona:hidden("editkey", editkey)%>
    4.39  		<%end
    4.40  		
    4.41  		local functions = { }
    4.42 @@ -57,7 +63,8 @@
    4.43  		if loona.authuser then
    4.44  			local lockfname = fname and (contentdir .. "/" .. fname)
    4.45  			
    4.46 -			if loona.useralert and editkey == "main" then
    4.47 +-- 			if loona.useralert and editkey == "main" then
    4.48 +			if loona.useralert and editkey == loona.args.editkey then
    4.49  				--
    4.50  				--	display user alert/request/confirmation
    4.51  				--
    4.52 @@ -225,7 +232,7 @@
    4.53  				loona.args.actionpublishprofile) and editkey == "main" then
    4.54  				hidden = true
    4.55  				local profiles = { }
    4.56 -				for p in pairs(loona.getprofiles(loona.config.contentdir, loona.lang)) do
    4.57 +				for p in pairs(loona:getprofiles()) do
    4.58  					table.insert(profiles, p)
    4.59  				end
    4.60  				table.sort(profiles)
    4.61 @@ -262,7 +269,7 @@
    4.62  								<legend>
    4.63  									<%=loona.locale.PUBLISHPROFILE%>
    4.64  								</legend>
    4.65 -								<%=loona.hidden("publishprofile", loona.profile)%>
    4.66 +								<%=loona:hidden("publishprofile", loona.profile)%>
    4.67  								<input type="submit" name="actionpublishprofile" value="<%=loona.locale.PUBLISH%>" />
    4.68  								<%hiddenvars()%>
    4.69  							</fieldset>
    4.70 @@ -272,7 +279,7 @@
    4.71  								<legend>
    4.72  									<%=loona.locale.DELETEPROFILE%>
    4.73  								</legend>
    4.74 -								<%=loona.hidden("deleteprofile", loona.profile)%>
    4.75 +								<%=loona:hidden("deleteprofile", loona.profile)%>
    4.76  								<input type="submit" name="actiondeleteprofile" value="<%=loona.locale.DELETE%>" />
    4.77  								<%hiddenvars()%>
    4.78  							</fieldset>
    4.79 @@ -283,17 +290,19 @@
    4.80  				if not loona.section.redirect then
    4.81  					extramsg = loona.profile == loona.pubprofile and
    4.82  						loona.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE
    4.83 -					edit = loadcontent(contentdir, fname):gsub("\194\160", "&nbsp;")
    4.84 +					edit = loadcontent(contentdir, fname):gsub("\194\160", "&nbsp;") -- TODO
    4.85  					changed = loona.section and (loona.section.revisiondate or loona.section.creationdate)
    4.86  				end
    4.87  			elseif loona.args.actionpreview and editkey == loona.args.editkey then
    4.88  				edit = loona.args.editform
    4.89 -				show = loona.domarkup(edit:gsub("&nbsp;", "\194\160"))
    4.90 +				show = loona:domarkup(edit:gsub("&nbsp;", "\194\160")) -- TODO
    4.91  			elseif loona.args.actionsave and editkey == loona.args.editkey then
    4.92  				local c = loona.args.editform
    4.93 +				local dynamic
    4.94 +				
    4.95  				if lockfname then
    4.96 -					loona.expire(contentdir, "[^.]%S+.LOCK")
    4.97 -					if loona.lockfile(lockfname) then
    4.98 +					loona:expire(contentdir, "[^.]%S+.LOCK")
    4.99 +					if loona:lockfile(lockfname) then
   4.100  						-- lock was expired, aquired a new one
   4.101  						extramsg = loona.locale.SECTION_COULD_HAVE_CHANGED
   4.102  						edit = c
   4.103 @@ -301,18 +310,12 @@
   4.104  						local tab = tek.source(lockfname .. ".LOCK")
   4.105  						if tab and tab.id == loona.session.id then
   4.106  							-- lock already held and is mine - try to save:
   4.107 -							local savec = c:gsub("&nbsp;", "\194\160")
   4.108 +							local savec = c:gsub("&nbsp;", "\194\160") -- TODO
   4.109  							os.remove(contentdir .. "/" .. savename .. ".html")
   4.110 -							local res = loona.savesection(contentdir, savename, savec)
   4.111 +							loona:savebody(savename, savec)
   4.112  							-- TODO: error handling
   4.113 -							loona.unlockfile(lockfname)
   4.114 -							local dynamic
   4.115 -							show, dynamic = loona.domarkup(savec)
   4.116 -							-- TODO: how to determine if ALL bodies are dynamic?
   4.117 -							-- if dynamic ~= loona.section.dynamic then
   4.118 -							-- 	loona.section.dynamic = dynamic
   4.119 -							-- 	loona.savesectionfile()
   4.120 -							-- end
   4.121 +							loona:unlockfile(lockfname)
   4.122 +							show, dynamic = loona:domarkup(savec)
   4.123  							changed = os.time()
   4.124  						else
   4.125  							-- lock was expired and someone else has it now
   4.126 @@ -322,21 +325,37 @@
   4.127  					end
   4.128  				else
   4.129  					-- new sidefile
   4.130 -					local savec = c:gsub("&nbsp;", "\194\160")
   4.131 -					loona.savesection(contentdir, savename, savec)
   4.132 +					local savec = c:gsub("&nbsp;", "\194\160") -- TODO
   4.133 +					loona:savebody(savename, savec)
   4.134  					-- TODO: error handling
   4.135 -					show = loona.domarkup(savec)
   4.136 +					show, dynamic = loona:domarkup(savec)
   4.137  				end
   4.138 +				
   4.139 +				-- mark dynamic text bodies
   4.140 +				if not loona.section.dynamic then
   4.141 +					loona.section.dynamic = { }
   4.142 +				end
   4.143 +				loona.section.dynamic[editkey] = dynamic
   4.144 +				local n = 0
   4.145 +				for _ in pairs(loona.section.dynamic) do
   4.146 +					n = n + 1
   4.147 +				end
   4.148 +				if n == 0 then
   4.149 +					loona.section.dynamic = nil
   4.150 +				end
   4.151 +				
   4.152 +				loona:saveindex()
   4.153 +				
   4.154  			elseif loona.args.actioncancel and editkey == loona.args.editkey then
   4.155  				if lockfname then
   4.156 -					loona.unlockfile(lockfname) -- remove lock
   4.157 +					loona:unlockfile(lockfname) -- remove lock
   4.158  				end
   4.159  			end
   4.160  			
   4.161  			if editkey == "main" and loona.section and loona.section.redirect then
   4.162  				table.insert(functions, function()%>
   4.163  					<h2><%=loona.locale.SECTION_IS_REDIRECT%></h2>
   4.164 -					<%=loona.link(loona.section.redirect)%>
   4.165 +					<%=loona:link(loona.section.redirect)%>
   4.166  					<hr />
   4.167  				<%end)
   4.168  			end
   4.169 @@ -344,8 +363,8 @@
   4.170  		end	
   4.171  		
   4.172  		if edit then
   4.173 -			loona.expire(contentdir, "[^.]%S+.LOCK")
   4.174 -			if fname and not loona.lockfile(contentdir .. "/" .. fname) then
   4.175 +			loona:expire(contentdir, "[^.]%S+.LOCK")
   4.176 +			if fname and not loona:lockfile(contentdir .. "/" .. fname) then
   4.177  				local tab = tek.source(contentdir .. "/" .. fname .. ".LOCK")
   4.178  				if tab and tab.id ~= loona.session.id then
   4.179  					extramsg = loona.locale.SECTION_IN_USE
   4.180 @@ -361,7 +380,7 @@
   4.181  						<legend>
   4.182  							<%=loona.locale.EDIT_SECTION%>
   4.183  						</legend>
   4.184 -						<textarea cols="80" rows="25" name="editform"><%=loona.encodeform(edit)%></textarea>
   4.185 +						<textarea cols="80" rows="25" name="editform"><%=loona:encodeform(edit)%></textarea>
   4.186  						<br />
   4.187  						<input type="submit" name="actionsave" value="<%=loona.locale.SAVE%>" />
   4.188  						<input type="submit" name="actionpreview" value="<%=loona.locale.PREVIEW%>" />
   4.189 @@ -374,14 +393,14 @@
   4.190  		
   4.191  		if not hidden then
   4.192  			table.insert(functions, function()
   4.193 -				loona.dosnippet(loona.config, function()
   4.194 +				loona:dosnippet(function()
   4.195  					if not show then
   4.196  						show = loadmarkup(contentdir, fname)
   4.197  						changed = loona.section and (loona.section.revisiondate or loona.section.creationdate)
   4.198  					end
   4.199 -					local parsed, msg = loona.loadhtml(show, "loona.out", "<parsed html>")
   4.200 +					local parsed, msg = loona:loadhtml(show, "loona:out", "<parsed html>")
   4.201  					assert(parsed, msg and "Syntax error : " .. msg)
   4.202 -					parsed()
   4.203 +					loona:runboxed(parsed)
   4.204  				end)
   4.205  			end)
   4.206  		end
   4.207 @@ -393,7 +412,7 @@
   4.208  					<%if editkey == "main" then%>
   4.209  						<a name="preview"></a>
   4.210  						<%=loona.authuser%> : 
   4.211 -						<%=loona.uilink(loona.sectionpath, "[" .. loona.locale.PROFILE .. "]", "actioneditprofiles=true", "editkey=" .. editkey)%> :
   4.212 +						<%=loona:uilink(loona.sectionpath, "[" .. loona.locale.PROFILE .. "]", "actioneditprofiles=true", "editkey=" .. editkey)%> :
   4.213  						<%=loona.profile%> (<%=loona.lang%>)
   4.214  						<%if loona.pubprofile == loona.profile then%>
   4.215  							<span class="warn">[<%=loona.locale.PUBLIC%>]</span>
   4.216 @@ -401,16 +420,16 @@
   4.217  						- <%=loona.sectionpath%>
   4.218  					<%end%>
   4.219  					<%if loona.section then%>
   4.220 -						<%=loona.uilink(loona.sectionpath, "[" .. loona.locale.EDIT .. "]", "actionedit=true", "editkey=" .. editkey)%> 
   4.221 +						<%=loona:uilink(loona.sectionpath, "[" .. loona.locale.EDIT .. "]", "actionedit=true", "editkey=" .. editkey)%> 
   4.222  						<%if editkey == "main" then%>
   4.223 -							- <%=loona.uilink(loona.sectionpath, "[" .. loona.locale.PROPERTIES .. "]", "actioneditprops=true", "editkey=" .. editkey)%>
   4.224 +							- <%=loona:uilink(loona.sectionpath, "[" .. loona.locale.PROPERTIES .. "]", "actioneditprops=true", "editkey=" .. editkey)%>
   4.225  						<%end%>
   4.226 -						<%if (editkey == "main" and not loona.section.subs) or (editkey ~= "main" and fname == savename) then%>
   4.227 -							- <%=loona.uilink(loona.sectionpath, "[" .. loona.locale.DELETE .. "]", "actiondelete=true", "editkey=" .. editkey)%>
   4.228 +						<%if fname == savename or (editkey == "main" and not loona.section.subs) then%>
   4.229 +							- <%=loona:uilink(loona.sectionpath, "[" .. loona.locale.DELETE .. "]", "actiondelete=true", "editkey=" .. editkey)%>
   4.230  						<%end%>
   4.231  						<%if editkey == "main" then%>
   4.232 -							- <%=loona.uilink(loona.sectionpath, "[" .. loona.locale.MOVEUP .. "]", "actionup=true", "editkey=" .. editkey)%>
   4.233 -							- <%=loona.uilink(loona.sectionpath, "[" .. loona.locale.MOVEDOWN .. "]", "actiondown=true", "editkey=" .. editkey)%>
   4.234 +							- <%=loona:uilink(loona.sectionpath, "[" .. loona.locale.MOVEUP .. "]", "actionup=true", "editkey=" .. editkey)%>
   4.235 +							- <%=loona:uilink(loona.sectionpath, "[" .. loona.locale.MOVEDOWN .. "]", "actiondown=true", "editkey=" .. editkey)%>
   4.236  						<%end%>
   4.237  						<%if changed and editkey == "main" then%>
   4.238  							- <%=loona.locale.CHANGED%>: <%=os.date("%d-%b-%Y %T", changed)%>
     5.1 --- a/cgi-bin/tek/web/markup.lua	Thu Mar 08 00:51:31 2007 +0100
     5.2 +++ b/cgi-bin/tek/web/markup.lua	Sat Mar 10 21:28:27 2007 +0100
     5.3 @@ -143,7 +143,7 @@
     5.4  
     5.5  	func = function(state, args)
     5.6  		state.is_dynamic_content = true
     5.7 -		local t = { '<%loona.include("', args[1], '"' }
     5.8 +		local t = { '<%loona:include("', args[1], '"' }
     5.9  		remove(args, 1)
    5.10  		for _, v in ipairs(args) do
    5.11  			insert(t, ',' .. v)
    5.12 @@ -194,9 +194,9 @@
    5.13  
    5.14  	link = function(state, link, isurl)
    5.15  		if isurl then
    5.16 -			return '<%=loona.elink("' .. cgi.encodeurl(link, true) .. '", [[', ']])%>'
    5.17 +			return '<%=loona:elink("' .. cgi.encodeurl(link, true) .. '", [[', ']])%>'
    5.18  		else
    5.19 -			return '<%=loona.link("' .. cgi.encodeurl(link, true) .. '", [[', ']])%>'
    5.20 +			return '<%=loona:link("' .. cgi.encodeurl(link, true) .. '", [[', ']])%>'
    5.21  		end
    5.22  	end,
    5.23  
     6.1 --- a/etc/config.lua.sample	Thu Mar 08 00:51:31 2007 +0100
     6.2 +++ b/etc/config.lua.sample	Sat Mar 10 21:28:27 2007 +0100
     6.3 @@ -26,13 +26,13 @@
     6.4  -- Locale directory -----------------------------------------------------------
     6.5  -- localedir = "../locale";
     6.6  
     6.7 --- Debugging facilities -------------------------------------------------------
     6.8 --- debug = false;
     6.9 +-- htdocs directory -----------------------------------------------------------
    6.10 +-- htdocsdir = "../htdocs";
    6.11  
    6.12  -- Cache content rendered to html snippets ------------------------------------
    6.13  -- cachehtml = false;
    6.14  
    6.15 --- Default/fallback section path ----------------------------------------------
    6.16 +-- Default/fallback section path. Must not be "index" -------------------------
    6.17  -- defname = "home";
    6.18  
    6.19  -- Default language -----------------------------------------------------------
     7.1 --- a/extensions/login.lua	Thu Mar 08 00:51:31 2007 +0100
     7.2 +++ b/extensions/login.lua	Sat Mar 10 21:28:27 2007 +0100
     7.3 @@ -1,11 +1,12 @@
     7.4  <%
     7.5  
     7.6 +local tek = require "tek"
     7.7  local args = require "tek.cgi.request.args"
     7.8  
     7.9  if loona.authuser then%>
    7.10  
    7.11  	<h3><%=loona.locale.LOGINFORM_LOGGEDAS%> <%=loona.authuser%>.</h3>
    7.12 -	<%=loona.plink(loona.sectionpath, loona.locale.LOGINFORM_LOGOUT,
    7.13 +	<%=loona:plink(loona.sectionpath, loona.locale.LOGINFORM_LOGOUT,
    7.14  		"login=false", "lang")%>
    7.15  
    7.16  <%else
    7.17 @@ -35,7 +36,7 @@
    7.18  				<input type="password" size="20" maxlength="40" name="password" />
    7.19  				<br />
    7.20  				<input type="submit" value="<%=loona.locale.LOGINFORM_LOGIN%>" />
    7.21 -				<%=loona.hidden("lang", args.lang)%>
    7.22 +				<%=loona:hidden("lang", args.lang)%>
    7.23  			</fieldset>
    7.24  		</form>
    7.25  	</div>
     8.1 --- a/extensions/search.lua	Thu Mar 08 00:51:31 2007 +0100
     8.2 +++ b/extensions/search.lua	Sat Mar 10 21:28:27 2007 +0100
     8.3 @@ -45,7 +45,7 @@
     8.4  		local matches = { }
     8.5  		for line in util.readdir(dir) do
     8.6  			if line:match(filepattern) then
     8.7 -				local tab, idx = loona.checkpath(loona.sections, line)
     8.8 +				local tab, idx = loona:checkpath(line)
     8.9  				if tab and not tab[idx].notvalid then
    8.10  					local s = io.open(dir .. "/" .. line)
    8.11  					if s then
    8.12 @@ -98,7 +98,7 @@
    8.13  			<ol>
    8.14  				<%for idx, item in ipairs(matches) do%>
    8.15  					<li>
    8.16 -						<%=loona.link(item.name, item.label)%>
    8.17 +						<%=loona:link(item.name, item.label)%>
    8.18  					</li>
    8.19  				<%end%>
    8.20  			</ol>
    8.21 @@ -117,9 +117,9 @@
    8.22  			</label>
    8.23  			<input class="searchtext" type="text" size="40" maxlength="40" name="searchtext" value="<%=searchtext%>" />
    8.24  			<input type="submit" value="<%=loona.locale.SEARCHFORM_SEARCH%>" />
    8.25 -			<%=loona.hidden("lang", args.lang)%>
    8.26 -			<%=loona.hidden("profile", loona.profile)%>
    8.27 -			<%=loona.hidden("session", loona.session and loona.session.id)%>
    8.28 +			<%=loona:hidden("lang", args.lang)%>
    8.29 +			<%=loona:hidden("profile", loona.profile)%>
    8.30 +			<%=loona:hidden("session", loona.session and loona.session.id)%>
    8.31  		</fieldset>
    8.32  	</form>
    8.33  </div>
     9.1 --- a/htdocs/index.lua	Thu Mar 08 00:51:31 2007 +0100
     9.2 +++ b/htdocs/index.lua	Sat Mar 10 21:28:27 2007 +0100
     9.3 @@ -6,9 +6,8 @@
     9.4  --	See copyright notice in COPYRIGHT
     9.5  --
     9.6  
     9.7 -require "loona"
     9.8 -
     9.9 -loona.setheader "Content-Type: text/html; charset=utf-8\n\n"
    9.10 +local os = require "os"
    9.11 +loona:setheader("Content-Type: text/html; charset=utf-8\n\n")
    9.12  
    9.13  %>
    9.14  
    9.15 @@ -19,38 +18,38 @@
    9.16  	<link rel="stylesheet" type="text/css" href='/loona.css' />
    9.17  	<meta http-equiv="content-type" content="text/html; charset=utf-8" />
    9.18  	<title>
    9.19 -		Loona CMS : <%=loona.encodeform(loona.title())%>
    9.20 +		Loona CMS : <%=loona:encodeform(loona:title())%>
    9.21  	</title>
    9.22  </head>
    9.23  <body>
    9.24  	<div id="container">
    9.25  		<div id="logo">
    9.26 -			<a href="<%=loona.href("home")%>"><img
    9.27 +			<a href="<%=loona:href("home")%>"><img
    9.28  				src="/images/loona.png" alt="LOona logo" /></a>
    9.29  		</div>
    9.30  		<div id="menu">
    9.31  			<h3>menu</h3>
    9.32 -			<%loona.menu()%>
    9.33 +			<%loona:menu()%>
    9.34  		</div>
    9.35  		<div id="top">
    9.36  			<h3>top body</h3>
    9.37  			<div class="body">
    9.38 -				<%loona.body("top")%>
    9.39 +				<%loona:body("top")%>
    9.40  			</div>
    9.41  		</div>
    9.42  		<div id="main">
    9.43  			<h3>main body</h3>
    9.44  			<div class="body">
    9.45 -				<%loona.body("main")%>
    9.46 +				<%loona:body("main")%>
    9.47  			</div>
    9.48  		</div>
    9.49  		<div id="side">
    9.50  			<h3>side body</h3>
    9.51  			<div class="body">
    9.52 -				<%loona.body("side")%>
    9.53 +				<%loona:body("side")%>
    9.54  			</div>
    9.55  		</div>
    9.56 -		<%if loona.config.debug then%>
    9.57 +		<%if loona.authuser then%>
    9.58  			<div id="debug">
    9.59  				Page took <%=os.clock()%>s to generate<br />
    9.60  				<a href="http://validator.w3.org/check?uri=referer"><img