cgi-bin/tek/class/loona.lua
changeset 198 87a4de7c7457
parent 189 efb1bd425c56
child 199 8b5fc485edf4
     1.1 --- a/cgi-bin/tek/class/loona.lua	Sun May 20 21:16:54 2007 +0200
     1.2 +++ b/cgi-bin/tek/class/loona.lua	Fri Oct 05 01:41:59 2007 +0200
     1.3 @@ -5,6 +5,7 @@
     1.4  --	See copyright notice in COPYRIGHT
     1.5  --
     1.6  
     1.7 +local Atom = require "tek.class.atom"
     1.8  local lib = require "tek.lib"
     1.9  local luahtml = require "tek.lib.luahtml"
    1.10  local posix = require "tek.os.posix"
    1.11 @@ -13,7 +14,7 @@
    1.12  local util = require "tek.class.loona.util"
    1.13  local markup = require "tek.class.loona.markup"
    1.14  
    1.15 -local boxed_G = { 
    1.16 +local boxed_G = {
    1.17  	string = string, table = table,
    1.18  	assert = assert, collectgarbage = collectgarbage, dofile = dofile,
    1.19  	error = error, getfenv = getfenv, getmetatable = getmetatable,
    1.20 @@ -31,61 +32,58 @@
    1.21  local open, remove, rename, getenv, time, date =
    1.22  	io.open, os.remove, os.rename, os.getenv, os.time, os.date
    1.23  
    1.24 +-------------------------------------------------------------------------------
    1.25 +--	Module setup:
    1.26 +-------------------------------------------------------------------------------
    1.27  
    1.28  module "tek.class.loona"
    1.29  
    1.30 +_VERSION = 4
    1.31 +_REVISION = 4
    1.32  
    1.33 -_VERSION = 4
    1.34 -_REVISION = 2
    1.35 +-------------------------------------------------------------------------------
    1.36 +--	class Session:
    1.37 +-------------------------------------------------------------------------------
    1.38  
    1.39 +local Session = Atom:newclass()
    1.40  
    1.41 --- Session
    1.42 +function Session.new(class, self)
    1.43  
    1.44 +	self = Atom.new(class, self or { })
    1.45  
    1.46 -local Session = { }
    1.47 +	assert(self.id, "No session Id")
    1.48 + 	assert(self.sessiondir, "No session directory")
    1.49  
    1.50 -
    1.51 -function Session:new(o)
    1.52 -
    1.53 -	o = o or { }
    1.54 - 	setmetatable(o, self)
    1.55 - 	self.__index = self
    1.56 -	
    1.57 -	assert(o.id, "No session Id")
    1.58 - 	assert(o.sessiondir, "No session directory")
    1.59 -	
    1.60 -	o.name = o.id:gsub("(.)", function(a)
    1.61 +	self.name = self.id:gsub("(.)", function(a)
    1.62  		return ("%02x"):format(a:byte())
    1.63  	end)
    1.64 -	o.filename = o.sessiondir .. "/" .. o.name
    1.65 +	self.filename = self.sessiondir .. "/" .. self.name
    1.66  	-- remove non-dotted files (expired sessions) from sessions dir:
    1.67 -	util.expire(o.sessiondir, "[^.]%S+", o.maxage or 600)
    1.68 +	util.expire(self.sessiondir, "[^.]%S+", self.maxage or 600)
    1.69  	-- load session state:
    1.70 -	o.data = lib.source(o.filename) or { }
    1.71 -	
    1.72 -	return o
    1.73 +	self.data = lib.source(self.filename) or { }
    1.74 +
    1.75 +	return self
    1.76  end
    1.77  
    1.78 -
    1.79  function Session:save()
    1.80  	local f = open(self.filename, "wb")
    1.81  	assert(f, "Failed to open session file for writing")
    1.82 -	lib.dump(self.data, function(...) 
    1.83 -		f:write(unpack(arg)) 
    1.84 +	lib.dump(self.data, function(...)
    1.85 +		f:write(...)
    1.86  	end)
    1.87  	f:close()
    1.88  end
    1.89  
    1.90 -
    1.91  function Session:delete()
    1.92  	remove(self.filename)
    1.93  end
    1.94  
    1.95 +-------------------------------------------------------------------------------
    1.96 +--	class Loona
    1.97 +-------------------------------------------------------------------------------
    1.98  
    1.99 --- LOona
   1.100 -
   1.101 -
   1.102 -local Loona = getfenv()
   1.103 +local Loona = Atom:newclass(getfenv())
   1.104  
   1.105  
   1.106  function Loona:dbmsg(msg, detail)
   1.107 @@ -169,9 +167,9 @@
   1.108  function Loona:publishprofile(profile, lang)
   1.109  	lang = lang or self.lang
   1.110  	local contentdir = self.config.contentdir
   1.111 -	
   1.112 +
   1.113  	-- Get languages for the current profile
   1.114 -	
   1.115 +
   1.116  	local plangs = { }
   1.117  	local lmatch = "^" .. self.profile .. "_(%w+)$"
   1.118  	for e in util.readdir(self.config.contentdir) do
   1.119 @@ -180,35 +178,39 @@
   1.120  			table.insert(plangs, l)
   1.121  		end
   1.122  	end
   1.123 -	
   1.124 +
   1.125  	-- For all languages, update "current" symlink
   1.126 -	
   1.127 +
   1.128  	for _, lang in ipairs(plangs) do
   1.129  		self:makecurrent(profile, lang)
   1.130  	end
   1.131 -	
   1.132 +
   1.133  	-- These arguments are overwritten globally and need to get restored
   1.134 -	
   1.135 +
   1.136  	local save_args = { self.args.lang, self.args.profile, self.args.session }
   1.137 -	
   1.138 +
   1.139  	-- For all languages, unroll site to static HTML
   1.140 -	
   1.141 +
   1.142  	for _, lang in ipairs(plangs) do
   1.143  		local ext = (#plangs == 1 and ".html") or (".html." .. lang)
   1.144  		self:recursesections(self.sections, function(self, s, e, path)
   1.145  			path = path and path .. "/" .. e.name or e.name
   1.146  			if not e.notvisible then
   1.147 -				Loona:dumphtml { requestpath = path, requestlang = lang,
   1.148 -					htmlext = ext, insecure = true }
   1.149 +				Loona:dumphtml {
   1.150 +					request = self.request, -- reuse request
   1.151 +					userdata = self.userdata, -- reuse userdata
   1.152 +					requestpath = path, requestlang = lang,
   1.153 +					htmlext = ext, insecure = true
   1.154 +				}
   1.155  			end
   1.156  			return path
   1.157  		end)
   1.158  	end
   1.159 -	
   1.160 +
   1.161  	-- Restore arguments
   1.162 -	
   1.163 +
   1.164  	self.args.lang, self.args.profile, self.args.session = unpack(save_args)
   1.165 -	
   1.166 +
   1.167  	-- Update file cache
   1.168  
   1.169  	local htdocs = self.config.htdocsdir
   1.170 @@ -223,7 +225,7 @@
   1.171   				self:dbmsg("Could not purge cached HTML file", msg))
   1.172  		end
   1.173  	end
   1.174 -	
   1.175 +
   1.176  	for e in util.readdir(cache) do
   1.177  		local f = e:match("^(.*%.html%.?%w*)%.tmp$")
   1.178  		if f then
   1.179 @@ -250,9 +252,9 @@
   1.180  
   1.181  function Loona:indexsections()
   1.182  	self:recursesections(self.sections, function(self, s, e)
   1.183 -		e.notvalid = (not self.secure and e.secure) or 
   1.184 -			(not self.authuser and e.secret) or nil
   1.185 -		e.notvisible = e.notvalid or not self.authuser and e.hidden or nil
   1.186 +		e.notvalid = (not self.secure and e.secure) or
   1.187 +			(not self.authuser_visible and e.secret) or nil
   1.188 +		e.notvisible = e.notvalid or not self.authuser_visible and e.hidden or nil
   1.189  		s[e.name] = e
   1.190  	end)
   1.191  end
   1.192 @@ -313,7 +315,7 @@
   1.193  	local fullname = self.contentdir .. "/" .. fname
   1.194  	local success, msg = remove(fullname)
   1.195  	if all_bodies then
   1.196 -		local pat = "^" .. 
   1.197 +		local pat = "^" .. -- TODO: check
   1.198  			fname:gsub("%^%$%(%)%%%.%[%]%*%+%-%?", "%%%1") .. "%..*$"
   1.199  		for e in util.readdir(self.contentdir) do
   1.200  			if e:match(pat) then
   1.201 @@ -390,7 +392,7 @@
   1.202  					res, idx = tab, i
   1.203  					tab = tab[i].subs
   1.204  				else
   1.205 -					res, idx = nil, nil
   1.206 +					res, idx, tab = nil, nil, nil
   1.207  				end
   1.208  			end
   1.209  		end)
   1.210 @@ -415,7 +417,7 @@
   1.211  	end
   1.212  	self:out("<h2>Error</h2>")
   1.213  	self:out("<h3>" .. self:encodeform(ret[2] or "") .. "</h3>")
   1.214 -	if self.authuser then
   1.215 +	if self.authuser_debug then
   1.216  		if type(ret[3]) == "string" then
   1.217  			self:out("<p>" .. self:encodeform(ret[3]) .. "</p>")
   1.218  		end
   1.219 @@ -423,11 +425,11 @@
   1.220  			self:out("<pre>" .. self:encodeform(ret[4]) .. "</pre>")
   1.221  		end
   1.222  	end
   1.223 -end	
   1.224 +end
   1.225  
   1.226  
   1.227  function Loona:lockfile(file)
   1.228 -	return not self.session and true or 
   1.229 +	return not self.session and true or
   1.230  		posix.symlink(self.session.filename, file .. ".LOCK")
   1.231  end
   1.232  
   1.233 @@ -481,7 +483,7 @@
   1.234  	local f, msg = open(fname2)
   1.235  	assert(f, self:dbmsg("Cannot open file", msg))
   1.236  	local parsed, msg = self:loadhtml(f, "loona:out", fname2)
   1.237 -	assert(parsed, self:dbmsg("Syntax error", msg))
   1.238 +	assert(parsed, self:dbmsg("Syntax error", msg and msg.text))
   1.239  	return self:runboxed(parsed, nil, unpack(arg))
   1.240  end
   1.241  
   1.242 @@ -491,7 +493,7 @@
   1.243  function Loona:shref(section, arg)
   1.244  	local args2 = { } -- propagated or new arguments
   1.245  	for _, a in ipairs(arg) do
   1.246 -		local key, val = a:match("^(%w+)=(.*)$")
   1.247 +		local key, val = a:match("^([%w_]+)=(.*)$")
   1.248  		if key and val then -- "arg=val" sets/overrides argument
   1.249  			table.insert(args2, { name = key, value = val })
   1.250  		elseif self.args[a] then -- just "arg" propagates argument
   1.251 @@ -587,7 +589,7 @@
   1.252  	end
   1.253  	table.sort(tab)
   1.254  	for _, v in ipairs(tab) do
   1.255 -		tab[v] = v	
   1.256 +		tab[v] = v
   1.257  	end
   1.258  	return tab
   1.259  end
   1.260 @@ -649,9 +651,9 @@
   1.261  function Loona:menu(level, recurse, render)
   1.262  	level = level or 1
   1.263  	render = render or { }
   1.264 -	render.link = render.link or 
   1.265 +	render.link = render.link or
   1.266  		function(self, level, path, label, active, ...)
   1.267 -			self:out(('<a %shref="%s">%s</a>\n'):format(active and 
   1.268 +			self:out(('<a %shref="%s">%s</a>\n'):format(active and
   1.269  				'class="active" ' or "", self:href(path, unpack(arg)), label))
   1.270  		end
   1.271  	render.listbegin = render.listbegin or
   1.272 @@ -672,7 +674,8 @@
   1.273  		end
   1.274  	recurse = recurse == nil and true or recurse
   1.275  	local path = level > 1 and self:getpath("/", level - 1) or nil
   1.276 -	local addnew = self.authuser and not self.ispubprofile
   1.277 +	local addnew = self.authuser_menu and
   1.278 +		(not self.ispubprofile or self.config.editablepubprofile)
   1.279  	self:rmenu(level, render, path, addnew, recurse)
   1.280  end
   1.281  
   1.282 @@ -695,24 +698,25 @@
   1.283  
   1.284  
   1.285  function Loona:editable(editkey, fname, savename)
   1.286 -	
   1.287 +
   1.288  	local contentdir = self.contentdir
   1.289  	local edit, show, hidden, extramsg, changed
   1.290 -	
   1.291 -	if self.authuser then
   1.292 -		
   1.293 +
   1.294 +	if self.authuser_edit or self.authuser_profile or self.authuser_modifyprofile or self_authuser_attr
   1.295 +		or self.authuser_menu then
   1.296 +
   1.297  		local hiddenvars = table.concat( {
   1.298  			self:hidden("lang", self.args.lang),
   1.299  			self:hidden("profile", self.profile),
   1.300  			self:hidden("session", self.session.id),
   1.301  			self:hidden("editkey", editkey) }, " ")
   1.302 -	
   1.303 +
   1.304  		local lockfname = fname and (contentdir .. "/" .. fname)
   1.305 -		
   1.306 +
   1.307  		if self.useralert and editkey == self.args.editkey then
   1.308 -			
   1.309 +
   1.310  			--	display user alert/request/confirmation
   1.311 -			
   1.312 +
   1.313  			hidden = true
   1.314  			self:out([[
   1.315  			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   1.316 @@ -725,11 +729,11 @@
   1.317  				</fieldset>
   1.318  			</form>
   1.319  			]])
   1.320 -			
   1.321 -		elseif self.args.actionnew and editkey == "main" then
   1.322 -			
   1.323 +
   1.324 +		elseif self.args.actionnew and editkey == "main" and self.authuser_menu then
   1.325 +
   1.326  			--	form for creating a new section
   1.327 -			
   1.328 +
   1.329  			hidden = true
   1.330  			if self.ispubprofile then
   1.331  				self:out([[<h2><span class="warn">]] .. self.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE ..[[</span></h2>]])
   1.332 @@ -797,15 +801,15 @@
   1.333  								<input size="30" maxlength="50" name="editredirect" />
   1.334  							</td>
   1.335  						</tr>
   1.336 -					</table>					
   1.337 +					</table>
   1.338  					<input type="submit" name="actioncreate" value="]] .. self.locale.CREATE .. [[" />
   1.339  					]] .. hiddenvars .. [[
   1.340  				</fieldset>
   1.341  			</form>
   1.342  			<hr />
   1.343  			]])
   1.344 -		
   1.345 -		elseif self.args.actioneditprops and editkey == "main" then
   1.346 +
   1.347 +		elseif self.args.actioneditprops and editkey == "main" and self.authuser_attr then
   1.348  			hidden = true
   1.349  			if self.ispubprofile then
   1.350  				self:out([[<h2><span class="warn">]] .. self.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE ..[[</span></h2>]])
   1.351 @@ -872,62 +876,70 @@
   1.352  				</fieldset>
   1.353  			</form>
   1.354  			]])
   1.355 -		
   1.356 +
   1.357  		elseif (self.args.actioneditprofiles or
   1.358 -			self.args.actioncreateprofile or 
   1.359 -			self.args.actionchangeprofile or 
   1.360 +			self.args.actioncreateprofile or
   1.361 +			self.args.actionchangeprofile or
   1.362  			self.args.actionchangelanguage or
   1.363 -			self.args.actionpublishprofile) and editkey == "main" then
   1.364 +			self.args.actionpublishprofile) and editkey == "main" and
   1.365 +			(self.authuser_profile or self.authuser_modifyprofile) then
   1.366  			hidden = true
   1.367 -			self:out([[
   1.368 -			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   1.369 -				<fieldset>
   1.370 -					<legend>
   1.371 -						]] .. self.locale.CHANGEPROFILE .. [[
   1.372 -					</legend>
   1.373 -					<select name="changeprofile" size="1">]])
   1.374 -						for _, val in ipairs(self:getprofiles()) do
   1.375 -							self:out('<option' .. (val == self.profile and ' selected="selected"' or '') .. '>')
   1.376 -							self:out(val)
   1.377 -							self:out('</option>')
   1.378 -						end
   1.379 -					self:out([[
   1.380 -					</select>							
   1.381 -					<input type="submit" name="actionchangeprofile" value="]] .. self.locale.CHANGE ..[[" />
   1.382 -					]] .. hiddenvars .. [[
   1.383 -				</fieldset>
   1.384 -			</form>
   1.385 -			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   1.386 -				<fieldset>
   1.387 -					<legend>
   1.388 -						]] .. self.locale.CHANGELANGUAGE .. [[
   1.389 -					</legend>
   1.390 -					<select name="changelanguage" size="1">]])
   1.391 -						for _, val in ipairs(self:getlanguages()) do
   1.392 -							self:out('<option' .. (val == self.lang and ' selected="selected"' or '') .. '>')
   1.393 -							self:out(val)
   1.394 -							self:out('</option>')
   1.395 -						end
   1.396 -					self:out([[
   1.397 -					</select>							
   1.398 -					<input type="submit" name="actionchangelanguage" value="]] .. self.locale.CHANGE ..[[" />
   1.399 -					]] .. hiddenvars .. [[
   1.400 -				</fieldset>
   1.401 -			</form>
   1.402 -			<form action="]] .. self.document ..[[" method="post" accept-charset="utf-8">
   1.403 -				<fieldset>
   1.404 -					<legend>
   1.405 -						]] .. self.locale.CREATEPROFILE .. [[
   1.406 -					</legend>
   1.407 -					<input size="20" maxlength="20" name="createprofile" />
   1.408 -					]] .. self.locale.LANGUAGE ..[[
   1.409 -					<input size="2" maxlength="2" name="createlanguage" value="]] .. self.lang ..[[" />
   1.410 -					<input type="submit" name="actioncreateprofile" value="]] .. self.locale.CREATE .. [[" />
   1.411 -					]] .. hiddenvars .. [[
   1.412 -				</fieldset>
   1.413 -			</form>
   1.414 -			]])
   1.415 -			if not self.ispubprofile then
   1.416 +			if self.authuser_profile then
   1.417 +				self:out([[
   1.418 +				<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   1.419 +					<fieldset>
   1.420 +						<legend>
   1.421 +							]] .. self.locale.CHANGEPROFILE .. [[
   1.422 +						</legend>
   1.423 +						<select name="changeprofile" size="1">]])
   1.424 +							for _, val in ipairs(self:getprofiles()) do
   1.425 +								self:out('<option' .. (val == self.profile and ' selected="selected"' or '') .. '>')
   1.426 +								self:out(val)
   1.427 +								self:out('</option>')
   1.428 +							end
   1.429 +						self:out([[
   1.430 +						</select>
   1.431 +						<input type="submit" name="actionchangeprofile" value="]] .. self.locale.CHANGE ..[[" />
   1.432 +						]] .. hiddenvars .. [[
   1.433 +					</fieldset>
   1.434 +				</form>
   1.435 +				<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   1.436 +					<fieldset>
   1.437 +						<legend>
   1.438 +							]] .. self.locale.CHANGELANGUAGE .. [[
   1.439 +						</legend>
   1.440 +						<select name="changelanguage" size="1">]])
   1.441 +							for _, val in ipairs(self:getlanguages()) do
   1.442 +								self:out('<option' .. (val == self.lang and ' selected="selected"' or '') .. '>')
   1.443 +								self:out(val)
   1.444 +								self:out('</option>')
   1.445 +							end
   1.446 +						self:out([[
   1.447 +						</select>
   1.448 +						<input type="submit" name="actionchangelanguage" value="]] .. self.locale.CHANGE ..[[" />
   1.449 +						]] .. hiddenvars .. [[
   1.450 +					</fieldset>
   1.451 +				</form>
   1.452 +				]])
   1.453 +			end
   1.454 +			if self.authuser_modifyprofile then
   1.455 +				self:out([[
   1.456 +				<form action="]] .. self.document ..[[" method="post" accept-charset="utf-8">
   1.457 +					<fieldset>
   1.458 +						<legend>
   1.459 +							]] .. self.locale.CREATEPROFILE .. [[
   1.460 +						</legend>
   1.461 +						<input size="20" maxlength="20" name="createprofile" />
   1.462 +						]] .. self.locale.LANGUAGE ..[[
   1.463 +						<input size="2" maxlength="2" name="createlanguage" value="]] .. self.lang ..[[" />
   1.464 +						<input type="submit" name="actioncreateprofile" value="]] .. self.locale.CREATE .. [[" />
   1.465 +						]] .. hiddenvars .. [[
   1.466 +					</fieldset>
   1.467 +				</form>
   1.468 +				]])
   1.469 +			end
   1.470 +			if not self.ispubprofile or self.config.editablepubprofile and
   1.471 +				self.authuser_modifyprofile then
   1.472  				self:out([[
   1.473  				<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   1.474  					<fieldset>
   1.475 @@ -951,23 +963,25 @@
   1.476  				</form>
   1.477  				]])
   1.478  			end
   1.479 -			
   1.480 +
   1.481  		elseif self.args.actionedit and editkey == self.args.editkey then
   1.482 -			if not self.section.redirect then
   1.483 +			if not self.section.redirect and self.authuser_edit then
   1.484  				extramsg = self.ispubprofile and
   1.485  					self.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE
   1.486  				edit = self:loadcontent(fname):gsub("\194\160", "&nbsp;") -- TODO
   1.487  				changed = self.section and (self.section.revisiondate or self.section.creationdate)
   1.488  			end
   1.489 -		
   1.490 -		elseif self.args.actionpreview and editkey == self.args.editkey then
   1.491 +
   1.492 +		elseif self.args.actionpreview and editkey == self.args.editkey and
   1.493 +			self.authuser_edit then
   1.494  			edit = self.args.editform
   1.495  			show = self:domarkup(edit:gsub("&nbsp;", "\194\160")) -- TODO
   1.496 -		
   1.497 -		elseif self.args.actionsave and editkey == self.args.editkey then
   1.498 +
   1.499 +		elseif self.args.actionsave and editkey == self.args.editkey
   1.500 +			and self.authuser_edit then
   1.501  			local c = self.args.editform
   1.502  			local dynamic
   1.503 -			
   1.504 +
   1.505  			if lockfname then
   1.506  				self:expire(contentdir, "[^.]%S+.LOCK")
   1.507  				if self:lockfile(lockfname) then
   1.508 @@ -998,7 +1012,7 @@
   1.509  				-- TODO: error handling
   1.510  				show, dynamic = self:domarkup(savec)
   1.511  			end
   1.512 -			
   1.513 +
   1.514  			-- mark dynamic text bodies
   1.515  			if not self.section.dynamic then
   1.516  				self.section.dynamic = { }
   1.517 @@ -1011,21 +1025,21 @@
   1.518  			if n == 0 then
   1.519  				self.section.dynamic = nil
   1.520  			end
   1.521 -			
   1.522 +
   1.523  			self:saveindex()
   1.524 -			
   1.525 +
   1.526  		elseif self.args.actioncancel and editkey == self.args.editkey then
   1.527  			if lockfname then
   1.528  				self:unlockfile(lockfname) -- remove lock
   1.529  			end
   1.530  		end
   1.531 -		
   1.532 +
   1.533  		if editkey == "main" and self.section and self.section.redirect then
   1.534  			self:out('<h2>' .. self.locale.SECTION_IS_REDIRECT ..'</h2>')
   1.535  			self:out(self:link(self.section.redirect))
   1.536  			self:out('<hr />')
   1.537  		end
   1.538 -	
   1.539 +
   1.540  		if edit then
   1.541  			self:expire(contentdir, "[^.]%S+.LOCK")
   1.542  			if fname and not self:lockfile(contentdir .. "/" .. fname) then
   1.543 @@ -1055,8 +1069,8 @@
   1.544  			</form>
   1.545  			]])
   1.546  		end
   1.547 -	end	
   1.548 -	
   1.549 +	end
   1.550 +
   1.551  	if not hidden then
   1.552  		self:dosnippet(function()
   1.553  			if not show then
   1.554 @@ -1068,32 +1082,37 @@
   1.555  			self:runboxed(parsed)
   1.556  		end)
   1.557  	end
   1.558 -	
   1.559 -	if self.authuser then
   1.560 +
   1.561 +	if self.authuser_profile or self.authuser_edit or self.authuser_menu or self.authuser_attr then
   1.562  		self:out([[
   1.563  		<hr />
   1.564  		<div class="edit">]])
   1.565  			if editkey == "main" then
   1.566  				self:out([[
   1.567  				<a name="preview"></a>
   1.568 -				]] .. self.authuser .. [[ : 
   1.569 -				]] .. self:uilink(self.sectionpath, "[" .. self.locale.PROFILE .. "]", "actioneditprofiles=true", "editkey=" .. editkey) .. [[ :
   1.570 -				]] .. self.profile .. "_" .. self.lang)
   1.571 -				if self.ispubprofile then
   1.572 -					self:out([[
   1.573 -						<span class="warn">[]] .. self.locale.PUBLIC .. [[]</span>]])
   1.574 +				]] .. self.authuser .. [[ : ]])
   1.575 +				if self.authuser_profile then
   1.576 +					self:out(self:uilink(self.sectionpath, "[" .. self.locale.PROFILE .. "]", "actioneditprofiles=true", "editkey=" .. editkey) .. [[ :
   1.577 +						]] .. self.profile .. "_" .. self.lang)
   1.578 +					if self.ispubprofile then
   1.579 +						self:out([[
   1.580 +							<span class="warn">[]] .. self.locale.PUBLIC .. [[]</span>]])
   1.581 +					end
   1.582 +					self:out(" : ")
   1.583  				end
   1.584 -				self:out(' : ' .. self.sectionpath .. ' ')
   1.585 +				self:out(self.sectionpath .. ' ')
   1.586  			end
   1.587 -			if self.section and not self.ispubprofile then
   1.588 -				self:out(self:uilink(self.sectionpath, "[" .. self.locale.EDIT .. "]", "actionedit=true", "editkey=" .. editkey) .. " ")
   1.589 -				if editkey == "main" then
   1.590 +			if self.section and (not self.ispubprofile or self.config.editablepubprofile) then
   1.591 +				if self.authuser_edit then
   1.592 +					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.EDIT .. "]", "actionedit=true", "editkey=" .. editkey) .. " ")
   1.593 +				end
   1.594 +				if editkey == "main" and self.authuser_attr then
   1.595  					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.PROPERTIES .. "]", "actioneditprops=true", "editkey=" .. editkey) .. " ")
   1.596  				end
   1.597 -				if fname == savename or not self.section.subs then
   1.598 +				if (fname == savename or not self.section.subs) and (self.authuser_edit and self.authuser_menu) then
   1.599  					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.DELETE .. "]", "actiondelete=true", "editkey=" .. editkey) .. " ")
   1.600  				end
   1.601 -				if editkey == "main" then
   1.602 +				if editkey == "main" and self.authuser_menu then
   1.603  					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.MOVEUP .. "]", "actionup=true", "editkey=" .. editkey) .. " ")
   1.604  					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.MOVEDOWN .. "]", "actiondown=true", "editkey=" .. editkey) .. " ")
   1.605  				end
   1.606 @@ -1117,7 +1136,7 @@
   1.607  		if menu.entries and menu.entries[menu.name] then
   1.608  			table.insert(t, menu.name)
   1.609  			local fn = table.concat(t, "_")
   1.610 -			if posix.stat(self.contentdir .. "/" .. fn .. ext, 
   1.611 +			if posix.stat(self.contentdir .. "/" .. fn .. ext,
   1.612  				"mode") == "file" then
   1.613  				path, section = fn, menu
   1.614  			end
   1.615 @@ -1137,10 +1156,10 @@
   1.616  
   1.617  
   1.618  function Loona:init()
   1.619 -	
   1.620 +
   1.621  	-- get list of languages, in order of preference
   1.622  	-- TODO: respect quality parameter, not just order
   1.623 -	
   1.624 +
   1.625  	local l = self.requestlang or self.args.lang
   1.626  	self.langs = { l and l:match("^%w+$") }
   1.627  	local s = getenv("HTTP_ACCEPT_LANGUAGE")
   1.628 @@ -1153,16 +1172,16 @@
   1.629  		end
   1.630  	end
   1.631  	table.insert(self.langs, self.config.deflang)
   1.632 -	
   1.633 +
   1.634  	-- get list of possible profiles
   1.635 -	
   1.636 +
   1.637  	local profiles = { }
   1.638  	for e in util.readdir(self.config.contentdir) do
   1.639  		profiles[e] = e
   1.640  	end
   1.641 -	
   1.642 +
   1.643  	-- get pubprofile
   1.644 -	
   1.645 +
   1.646  	for _, lang in ipairs(self.langs) do
   1.647  		local p = posix.readlink(self.config.contentdir .. "/current_" .. lang)
   1.648  		p = p and p:match("^(%w+)_" .. lang .. "$")
   1.649 @@ -1171,11 +1190,11 @@
   1.650  			break
   1.651  		end
   1.652  	end
   1.653 -	
   1.654 +
   1.655  	-- get profile
   1.656 -	
   1.657 +
   1.658  	local checkprofile =
   1.659 -		self.authuser and self.args.profile or self.pubprofile or "work"
   1.660 +		self.authuser_profile and self.args.profile or self.pubprofile or "work"
   1.661  	for _, lang in ipairs(self.langs) do
   1.662  		if profiles[checkprofile .. "_" .. lang] then
   1.663  			self.profile = checkprofile
   1.664 @@ -1183,44 +1202,44 @@
   1.665  			break
   1.666  		end
   1.667  	end
   1.668 -	
   1.669 +
   1.670  	assert(self.profile and self.lang, "Invalid profile or language")
   1.671 -	
   1.672 -	
   1.673 +
   1.674 +
   1.675  	self.ispubprofile = self.profile == self.pubprofile
   1.676 -	
   1.677 +
   1.678  	-- write back language and profile
   1.679 -	
   1.680 +
   1.681  	self.args.lang = (self.explicitlang or self.lang ~= self.config.deflang)
   1.682  		and self.lang or nil
   1.683  	self.args.profile = self.profile
   1.684 -	
   1.685 +
   1.686  	-- determine content directory pathname and section filename
   1.687 -	
   1.688 +
   1.689  	self.contentdir =
   1.690  		("%s/%s_%s"):format(self.config.contentdir, self.profile, self.lang)
   1.691   	self.indexfname = self.contentdir .. "/.sections"
   1.692 -	
   1.693 +
   1.694  	-- load sections
   1.695 -	
   1.696 +
   1.697   	self.sections = lib.source(self.indexfname)
   1.698 -	
   1.699 +
   1.700  	-- index sections, determine visibility in menu
   1.701 -	
   1.702 +
   1.703  	self:indexsections()
   1.704 -	
   1.705 +
   1.706  	-- decompose request path, produce a stack of sections
   1.707 -	
   1.708 +
   1.709  	self.submenus, self.section = self:getsection(self.requestpath)
   1.710  
   1.711  	-- handle redirects if not logged on
   1.712 -	
   1.713 -	if not self.authuser and self.section and self.section.redirect then
   1.714 +
   1.715 +	if not self.authuser_edit and self.section and self.section.redirect then
   1.716  		self.submenus, self.section = self:getsection(self.section.redirect)
   1.717  	end
   1.718 -			
   1.719 +
   1.720  	-- section path and document name (refined)
   1.721 -	
   1.722 +
   1.723  	self.sectionpath = self:getpath()
   1.724  	self.sectionname = self:getpath("_")
   1.725  
   1.726 @@ -1228,22 +1247,22 @@
   1.727  
   1.728  
   1.729  function Loona:handlechanges()
   1.730 -	
   1.731 +
   1.732  	local save
   1.733  
   1.734  	if self.args.editkey == "main" then
   1.735 -		
   1.736 +
   1.737  		-- In main editable section:
   1.738 -		
   1.739 +
   1.740  		if self.args.actioncreate then
   1.741 -			
   1.742 +
   1.743  			-- Create new section
   1.744 -			
   1.745 +
   1.746  			local editname = self.args.editname:lower()
   1.747  			assert(not editname:match("%W"),
   1.748  				self:dbmsg("Invalid section name", editname))
   1.749  			if not (section and (section.subs or section)[editname]) then
   1.750 -				local newpath = (self.sectionpath and 
   1.751 +				local newpath = (self.sectionpath and
   1.752  					(self.sectionpath .. "/")) .. editname
   1.753  				local s = self:addpath(newpath, { name = editname,
   1.754  					label = self.args.editlabel ~= "" and
   1.755 @@ -1259,19 +1278,19 @@
   1.756  					creationdate = time() })
   1.757  				save = true
   1.758  			end
   1.759 -		
   1.760 +
   1.761  		elseif self.args.actionsave then
   1.762 -			
   1.763 +
   1.764  			-- Save section
   1.765 -			
   1.766 +
   1.767  			self.section.revisiondate = time()
   1.768  			self.section.revisioner = self.authuser
   1.769  			save = true
   1.770 - 		
   1.771 +
   1.772  		elseif self.args.actionsaveprops then
   1.773 -			
   1.774 +
   1.775  			-- Save properties
   1.776 -			
   1.777 +
   1.778  			self.section.hidden = self.args.editvisibility and true
   1.779  			self.section.secret = self.args.editsecrecy and true
   1.780  			self.section.secure = self.args.editsecure and true
   1.781 @@ -1282,15 +1301,15 @@
   1.782  			self.section.redirect =
   1.783  				self.args.editredirect ~= "" and self.args.editredirect or nil
   1.784  			save = true
   1.785 -		
   1.786 +
   1.787  		elseif self.args.actionup then
   1.788 -			
   1.789 +
   1.790  			-- Move section up
   1.791 -			
   1.792 +
   1.793  			local t, i = self:checkpath(self.sectionpath)
   1.794  			if t and i > 1 then
   1.795  				if self.ispubprofile and not self.args.actionconfirm then
   1.796 -					useralert = {
   1.797 +					self.useralert = {
   1.798  						text = self.locale.ALERT_MOVE_IN_PUBLISHED_PROFILE,
   1.799  						confirm =
   1.800  							'<input type="submit" name="actionup" value="' ..
   1.801 @@ -1303,15 +1322,15 @@
   1.802  					save = true
   1.803  				end
   1.804  			end
   1.805 -		
   1.806 +
   1.807  		elseif self.args.actiondown then
   1.808 -			
   1.809 +
   1.810  			-- Move section down
   1.811 -			
   1.812 +
   1.813  			local t, i = self:checkpath(self.sectionpath)
   1.814  			if t and i < #t then
   1.815  				if self.ispubprofile and not self.args.actionconfirm then
   1.816 -					useralert = {
   1.817 +					self.useralert = {
   1.818  						text = self.locale.ALERT_MOVE_IN_PUBLISHED_PROFILE,
   1.819  						confirm =
   1.820  							'<input type="submit" name="actiondown" value="' ..
   1.821 @@ -1324,11 +1343,11 @@
   1.822  					save = true
   1.823  				end
   1.824  			end
   1.825 -		
   1.826 +
   1.827  		elseif self.args.actioncreateprofile and self.args.createprofile then
   1.828 -			
   1.829 +
   1.830  			-- Create profile
   1.831 -			
   1.832 +
   1.833  			local c = self.args.createprofile
   1.834  			if c == "" then
   1.835  				c = self.profile
   1.836 @@ -1336,7 +1355,7 @@
   1.837  			c = self:checkprofilename(c:lower())
   1.838  			local l = self:checklanguage((self.args.createlanguage or self.lang):lower())
   1.839  			if c == self.profile and l == self.lang then
   1.840 -				useralert = { 
   1.841 +				self.useralert = {
   1.842  					text = self.locale.ALERT_CANNOT_COPY_PROFILE_TO_SELF,
   1.843  					returnto = self:hidden("actioneditprofiles", "true")
   1.844  				}
   1.845 @@ -1349,7 +1368,7 @@
   1.846  					text = self.locale.ALERT_OVERWRITE_EXISTING_PROFILE
   1.847  				end
   1.848  				if text and not self.args.actionconfirm then
   1.849 -					useralert = {
   1.850 +					self.useralert = {
   1.851  						text = text,
   1.852  						returnto = self:hidden("actioneditprofiles", "true"),
   1.853  						confirm = '<input type="submit" ' ..
   1.854 @@ -1366,11 +1385,11 @@
   1.855  					self:copyprofile(c, self.profile, l, self.lang)
   1.856  				end
   1.857  			end
   1.858 -		
   1.859 +
   1.860  		elseif self.args.actiondeleteprofile and self.args.deleteprofile then
   1.861 -			
   1.862 +
   1.863  			-- Delete profile
   1.864 -			
   1.865 +
   1.866  			local c = self:checkprofilename(self.args.deleteprofile:lower())
   1.867  			assert(c ~= self.pubprofile,
   1.868  				self:dbmsg("Cannot delete published profile", c))
   1.869 @@ -1381,47 +1400,47 @@
   1.870  				self:init()
   1.871  				save = true
   1.872  			else
   1.873 -				useralert = { 
   1.874 +				self.useralert = {
   1.875  					text = self.locale.ALERT_DELETE_PROFILE,
   1.876  					returnto = self:hidden("actioneditprofiles", "true"),
   1.877  					confirm = '<input type="submit" ' ..
   1.878 -						'name="actiondeleteprofile" value="' .. 
   1.879 +						'name="actiondeleteprofile" value="' ..
   1.880  						self.locale.DELETE .. '" /> ' ..
   1.881  						self:hidden("actionconfirm", "true") ..
   1.882  						self:hidden("deleteprofile", c)
   1.883  				}
   1.884  			end
   1.885 -		
   1.886 +
   1.887  		elseif self.args.actionchangeprofile and self.args.changeprofile then
   1.888 -			
   1.889 +
   1.890  			-- Change profile
   1.891 -			
   1.892 +
   1.893  			local c = self:checkprofilename(self.args.changeprofile:lower())
   1.894  			self.profile = c
   1.895  			self.args.profile = c
   1.896  			save = true
   1.897 -		
   1.898 +
   1.899  		elseif self.args.actionchangelanguage and self.args.changelanguage then
   1.900 -			
   1.901 +
   1.902  			-- Change language
   1.903 -			
   1.904 +
   1.905  			local l = self:checklanguage(self.args.changelanguage:lower())
   1.906  			self.lang = l
   1.907  			self.args.lang = l
   1.908   			self.explicitlang = l
   1.909  			save = true
   1.910 -		
   1.911 +
   1.912  		elseif self.args.actionpublishprofile and self.args.publishprofile then
   1.913 -			
   1.914 +
   1.915  			-- Publish profile
   1.916 -			
   1.917 +
   1.918  			local c = self:checkprofilename(self.args.publishprofile:lower())
   1.919  			if c ~= self.publicprofile then
   1.920  				if self.args.actionconfirm then
   1.921  					self:publishprofile(c)
   1.922  					save = true
   1.923  				else
   1.924 -					useralert = {
   1.925 +					self.useralert = {
   1.926  						text = self.locale.ALERT_PUBLISH_PROFILE,
   1.927  						returnto = self:hidden("actioneditprofiles", "true"),
   1.928  						confirm = '<input type="submit" ' ..
   1.929 @@ -1433,20 +1452,20 @@
   1.930  				end
   1.931  			end
   1.932  		end
   1.933 -		
   1.934 +
   1.935  	end
   1.936 -	
   1.937 +
   1.938  	if self.args.actiondelete then
   1.939 -		
   1.940 +
   1.941  		-- Delete section
   1.942 -		
   1.943 +
   1.944  		if not self.args.actionconfirm then
   1.945 -			useralert = {
   1.946 +			self.useralert = {
   1.947  				text = self.ispubprofile and
   1.948  					self.locale.ALERT_DELETE_IN_PUBLISHED_PROFILE or
   1.949  					self.locale.ALERT_DELETE_SECTION,
   1.950  				confirm =
   1.951 -					'<input type="submit" name="actiondelete" value="' .. 
   1.952 +					'<input type="submit" name="actiondelete" value="' ..
   1.953  					self.locale.DELETE .. '" /> ' ..
   1.954  					self:hidden("actionconfirm", "true")
   1.955  			}
   1.956 @@ -1472,12 +1491,12 @@
   1.957  			save = true
   1.958  		end
   1.959  	end
   1.960 -		
   1.961 +
   1.962  	if save then
   1.963  		self:saveindex()
   1.964  		self:init()
   1.965  	end
   1.966 -	
   1.967 +
   1.968  end
   1.969  
   1.970  
   1.971 @@ -1501,51 +1520,48 @@
   1.972  end
   1.973  
   1.974  
   1.975 -function Loona:new(o)
   1.976 +function Loona.new(class, self)
   1.977 +
   1.978 +	self = Atom.new(class, self or { })
   1.979  
   1.980  	local parsed, msg
   1.981 -	
   1.982 -	o = o or { }
   1.983 -	setmetatable(o, self)
   1.984 -	self.__index = self
   1.985 --- 	o = atom.new(self, o)
   1.986 -	
   1.987 +
   1.988  	-- Buffer
   1.989 -	
   1.990 -	o.out = o.out or function(self, s)
   1.991 +
   1.992 +	self.out = self.out or function(self, s)
   1.993  		self.buf:out(s)
   1.994  	end
   1.995 -	o.setheader = o.setheader or function(self, s)
   1.996 -		self.buf:setheader(s)
   1.997 +	self.addheader = self.addheader or function(self, s)
   1.998 +		self.buf:addheader(s)
   1.999  	end
  1.1000 -	
  1.1001 +
  1.1002  	-- Get configuration
  1.1003 -	
  1.1004 -	o.config = o.config or lib.source(o.conffile or "../etc/config.lua") or { }
  1.1005 -	o.config.defname = o.config.defname or "home"
  1.1006 -	o.config.deflang = o.config.deflang or "en"
  1.1007 -	o.config.sessionmaxage = o.config.sessionmaxage or 6000
  1.1008 -	o.config.secureport = o.config.secureport or 443
  1.1009 -	o.config.passwdfile =
  1.1010 -		posix.abspath(o.config.passwdfile or "../etc/passwd.lua")
  1.1011 -	o.config.sessiondir =
  1.1012 -		posix.abspath(o.config.sessiondir or "../var/sessions")
  1.1013 -	o.config.extdir = posix.abspath(o.config.extdir or "../extensions")
  1.1014 -	o.config.contentdir = posix.abspath(o.config.contentdir or "../content")
  1.1015 -	o.config.localedir = posix.abspath(o.config.localedir or "../locale")
  1.1016 -	o.config.htdocsdir = posix.abspath(o.config.htdocsdir or "../htdocs")
  1.1017 -	o.config.htmlcachedir = 
  1.1018 -		posix.abspath(o.config.htmlcachedir or "../var/htmlcache")
  1.1019 -	o.config.extlinkextra = o.config.extlinksamewindow and ' class="extlink"'
  1.1020 +
  1.1021 +	self.config = self.config or lib.source(self.conffile or "../etc/config.lua") or { }
  1.1022 +	self.config.defname = self.config.defname or "home"
  1.1023 +	self.config.deflang = self.config.deflang or "en"
  1.1024 +	self.config.sessionmaxage = self.config.sessionmaxage or 6000
  1.1025 +	self.config.secureport = self.config.secureport or 443
  1.1026 +	self.config.passwdfile =
  1.1027 +		posix.abspath(self.config.passwdfile or "../etc/passwd.lua")
  1.1028 +	self.config.sessiondir =
  1.1029 +		posix.abspath(self.config.sessiondir or "../var/sessions")
  1.1030 +	self.config.extdir = posix.abspath(self.config.extdir or "../extensions")
  1.1031 +	self.config.contentdir = posix.abspath(self.config.contentdir or "../content")
  1.1032 +	self.config.localedir = posix.abspath(self.config.localedir or "../locale")
  1.1033 +	self.config.htdocsdir = posix.abspath(self.config.htdocsdir or "../htdocs")
  1.1034 +	self.config.htmlcachedir =
  1.1035 +		posix.abspath(self.config.htmlcachedir or "../var/htmlcache")
  1.1036 +	self.config.extlinkextra = self.config.extlinksamewindow and ' class="extlink"'
  1.1037  		or ' class="extlink" onclick="void(window.open(this.href, \'\', \'\')); return false;"'
  1.1038 -	
  1.1039 +
  1.1040  	-- Create proxy for on-demand loading of locales
  1.1041 -	
  1.1042 -	o.locale = { }
  1.1043 +
  1.1044 +	self.locale = { }
  1.1045  	local locmt = { }
  1.1046  	locmt.__index = function(_, key)
  1.1047 -		for _, l in ipairs(o.langs) do
  1.1048 -			locmt.__locale = lib.source(o.config.localedir .. "/" .. l)
  1.1049 +		for _, l in ipairs(self.langs) do
  1.1050 +			locmt.__locale = lib.source(self.config.localedir .. "/" .. l)
  1.1051  			if locmt.__locale then
  1.1052  				break
  1.1053  			end
  1.1054 @@ -1555,91 +1571,110 @@
  1.1055  		end
  1.1056  		return locmt.__locale[key] or key
  1.1057  	end
  1.1058 -	setmetatable(o.locale, locmt)
  1.1059 -	
  1.1060 +	setmetatable(self.locale, locmt)
  1.1061 +
  1.1062  	-- Get request, args, document, script name, request path
  1.1063 -	
  1.1064 -	o.request = o.request or Request:new()
  1.1065 -	o.args = o.request:getargs()
  1.1066 -	o.cgi_document = o.request:getdocument()
  1.1067 - 	
  1.1068 - 	o.scriptpath = o.scriptpath or o.cgi_document.Path
  1.1069 -	o.requesthandler = o.requesthandler or o.cgi_document.Handler
  1.1070 - 	o.requestdocument = o.requestdocument or o.cgi_document.Name
  1.1071 -	o.requestpath = o.requestpath or o.cgi_document.VirtualPath
  1.1072 -	o.explicitlang = not o.requestlang and o.args.lang
  1.1073 -	o.secure = not o.insecure and (o.request.Port == o.config.secureport)
  1.1074 +
  1.1075 +	self.request = self.request or Request:new()
  1.1076 +	self.args = self.request:getargs()
  1.1077 +	self.cgi_document = self.request:getdocument()
  1.1078 +
  1.1079 +--  	self.scriptpath = self.scriptpath or self.cgi_document.Path
  1.1080 +	self.requesthandler = self.requesthandler or self.cgi_document.Handler
  1.1081 + 	self.requestdocument = self.requestdocument or self.cgi_document.Name
  1.1082 +	self.requestpath = self.requestpath or self.cgi_document.VirtualPath
  1.1083 +	self.explicitlang = not self.requestlang and self.args.lang
  1.1084 +	self.secure = not self.insecure and (self.request.SERVER_PORT == self.config.secureport)
  1.1085  
  1.1086  	-- Manage login and establish session
  1.1087 -	
  1.1088 -	if not o.nologin then
  1.1089 -		local sid = o.args.session or o.request.UniqueID
  1.1090 -		o.session = o.session or Session:new {
  1.1091 +
  1.1092 +	if not self.nologin then
  1.1093 +		local sid = self.args.session or self.request.UNIQUE_ID
  1.1094 +		self.session = self.session or Session:new {
  1.1095  			id = sid,
  1.1096 -			sessiondir = o.config.sessiondir,
  1.1097 -			maxage = o.config.sessionmaxage
  1.1098 +			sessiondir = self.config.sessiondir,
  1.1099 +			maxage = self.config.sessionmaxage
  1.1100  		}
  1.1101 -		if o.args.login then
  1.1102 +		if self.args.login then
  1.1103  			-- write back session ID into request args:
  1.1104 -			o.args.session = sid -- !
  1.1105 -			if o.args.login == "false" then
  1.1106 -				o.session:delete()
  1.1107 -				o.session = nil
  1.1108 -			elseif o.args.password then
  1.1109 -				o.loginfailed = true
  1.1110 -				local pwddb = lib.source(o.config.passwdfile)
  1.1111 -				local pwdentry = pwddb[o.args.login]
  1.1112 -				if pwdentry and pwdentry.password == o.args.password then
  1.1113 -					o.session.data.authuser = pwdentry.username
  1.1114 -					o.session.data.id = o.session.id
  1.1115 -					o.loginfailed = nil
  1.1116 +			self.args.session = sid -- !
  1.1117 +			if self.args.login == "false" then
  1.1118 +				self.session:delete()
  1.1119 +				self.session = nil
  1.1120 +			elseif self.args.password then
  1.1121 +				self.loginfailed = true
  1.1122 +				local match, username, perm =
  1.1123 +					self:checkpw(self.args.login, self.args.password)
  1.1124 +				if match then
  1.1125 +					self.session.data.authuser = self.args.login
  1.1126 +					self.session.data.username = username
  1.1127 +					self.session.data.permissions = perm
  1.1128 +					self.session.data.id = self.session.id
  1.1129 +					self.loginfailed = nil
  1.1130  				end
  1.1131  			end
  1.1132  		end
  1.1133 -		o.authuser = o.session and o.session.data.authuser
  1.1134 +		self.authuser = self.session and self.session.data.authuser
  1.1135  	end
  1.1136  
  1.1137 -	if o.nologin or not o.authuser then
  1.1138 -		o.authuser = nil
  1.1139 -		o.session = nil
  1.1140 -		o.args.session = nil
  1.1141 +	if self.nologin or not self.authuser then
  1.1142 +		self.authuser = nil
  1.1143 +		self.session = nil
  1.1144 +		self.args.session = nil
  1.1145 +	else
  1.1146 +		self.authuser_edit = self.session.data.permissions:find("e") and true
  1.1147 +		self.authuser_menu = self.session.data.permissions:find("m") and true
  1.1148 +		self.authuser_attr = self.session.data.permissions:find("a") and true
  1.1149 +		self.authuser_profile = self.session.data.permissions:find("p") and true
  1.1150 +		self.authuser_modifyprofile = self.session.data.permissions:find("c") and true
  1.1151 +		self.authuser_visible = self.session.data.permissions:find("v") and true
  1.1152 +		self.authuser_debug = self.session.data.permissions:find("d") and true
  1.1153  	end
  1.1154  
  1.1155  	-- Get lang, locale, profile, section
  1.1156  
  1.1157 -	o:init()
  1.1158 -	if o.authuser then
  1.1159 -		o:handlechanges()
  1.1160 +	self:init()
  1.1161 +	if self.authuser then -- TODO?
  1.1162 +		self:handlechanges()
  1.1163  	else
  1.1164 -		o.args.profile = nil
  1.1165 +		self.args.profile = nil
  1.1166  	end
  1.1167 -	
  1.1168 +
  1.1169  	-- Current document
  1.1170 -	
  1.1171 -	o.document = o.requestdocument .. "/" .. o.sectionpath
  1.1172 -	if o.authuser then
  1.1173 -		o.getdocname = function(self, path)
  1.1174 +
  1.1175 +	self.document = self.requestdocument .. "/" .. self.sectionpath
  1.1176 +	if self.authuser then
  1.1177 +		self.getdocname = function(self, path)
  1.1178  			return self.requestdocument .. "/" .. (path or self.sectionpath)
  1.1179  		end
  1.1180  	else
  1.1181 -		o.getdocname = function(self, path, haveargs)
  1.1182 -			local dyn
  1.1183 -			dyn, path = self:isdynamic(path or self.sectionpath)
  1.1184 -			if dyn or haveargs then
  1.1185 +		self.getdocname = function(self, path, haveargs)
  1.1186 +			local dyn, exists
  1.1187 +			dyn, path, exists = self:isdynamic(path or self.sectionpath)
  1.1188 +			if dyn or haveargs or not exists then
  1.1189  				return self.requestdocument .. "/" .. path
  1.1190  			end
  1.1191  			path = path == self.config.defname and "index" or path
  1.1192  			return "/" .. path:gsub("/", "_") .. ".html"
  1.1193  		end
  1.1194  	end
  1.1195 -	
  1.1196 +
  1.1197  	-- Save session state
  1.1198 -	
  1.1199 -	if o.session then
  1.1200 -		o.session:save()
  1.1201 +
  1.1202 +	if self.session then
  1.1203 +		self.session:save()
  1.1204  	end
  1.1205 -	
  1.1206 -	return o
  1.1207 +
  1.1208 +	return self
  1.1209 +end
  1.1210 +
  1.1211 +
  1.1212 +function Loona:checkpw(login, passwd)
  1.1213 +	local pwddb = lib.source(self.config.passwdfile)
  1.1214 +	local pwdentry = pwddb[login]
  1.1215 +	if pwdentry and pwdentry.password == passwd then
  1.1216 +		return true, pwdentry.username, pwdentry.permissions or ""
  1.1217 +	end
  1.1218  end
  1.1219  
  1.1220  
  1.1221 @@ -1686,12 +1721,16 @@
  1.1222  
  1.1223  function Loona:isdynamic(path)
  1.1224  	path = path or self.sectionpath
  1.1225 +	local exists
  1.1226  	local t, i = self:checkpath(path)
  1.1227 -	if t and t[i].redirect then
  1.1228 -		path = t[i].redirect
  1.1229 -		t, i = self:isdynamic(path) -- TODO: prohibit endless recursion
  1.1230 +	if t then
  1.1231 +		exists = true
  1.1232 +		if t[i].redirect then
  1.1233 +			path = t[i].redirect
  1.1234 +			t, i, exists = self:isdynamic(path) -- TODO: prohibit endless recursion
  1.1235 +		end
  1.1236  	end
  1.1237 -	return t and t[i].dynamic, path
  1.1238 +	return t and t[i].dynamic, path, exists
  1.1239  end
  1.1240  
  1.1241  
  1.1242 @@ -1700,7 +1739,8 @@
  1.1243  	o = o or { }
  1.1244  	o.nologin = true
  1.1245  	o.out = function(self, s) table.insert(outbuf, s) end
  1.1246 -	o.setheader = function(self, s) end
  1.1247 +	o.addheader = function(self, s) end
  1.1248 +
  1.1249  	o = self:new(o):run()
  1.1250  	if not o:isdynamic() then
  1.1251  		local path = o.sectionname
  1.1252 @@ -1708,10 +1748,12 @@
  1.1253  		local srcname = o.config.htdocsdir .. "/" .. path .. o.htmlext
  1.1254  		local fh, msg = open(srcname .. ".tmp", "wb")
  1.1255  		assert(fh, self:dbmsg("Could not write cached HTML", msg))
  1.1256 -		fh:write(unpack(outbuf))
  1.1257 +		for _, line in ipairs(outbuf) do
  1.1258 +			fh:write(line)
  1.1259 +		end
  1.1260  		fh:close()
  1.1261  		local dstname = o.config.htmlcachedir .. "/" .. path .. o.htmlext
  1.1262  		local success, msg = posix.symlink(srcname, dstname .. ".tmp")
  1.1263 --- 		assert(success, self:dbmsg("Could not link to cached HTML", msg))
  1.1264 +		-- assert(success, self:dbmsg("Could not link to cached HTML", msg))
  1.1265  	end
  1.1266  end