cgi-bin/loona.lua
author Timm S. Mueller <tmueller@neoscientists.org>
Sun, 11 Mar 2007 20:35:16 +0100
changeset 150 9772192829c1
parent 149 8fc5dbd1eacb
child 151 e01bdf886128
permissions -rw-r--r--
rollout to static pages now emulates an insecure connection
     1 
     2 --
     3 --	loona - tiny CMS
     4 --	Written by Timm S. Mueller <tmueller at neoscientists.org>
     5 --	See copyright notice in COPYRIGHT
     6 --
     7 
     8 local tek = require "tek"
     9 local cgi = require "tek.cgi"
    10 local posix = require "tek.posix"
    11 require "tek.cgi.request"
    12 require "tek.cgi.request.args"
    13 require "tek.cgi.session"
    14 require "tek.web"
    15 require "tek.web.markup"
    16 require "tek.util"
    17 
    18 
    19 local boxed_G = { 
    20 	string = string, table = table,
    21 	assert = assert, collectgarbage = collectgarbage, dofile = dofile,
    22 	error = error, getfenv = getfenv, getmetatable = getmetatable,
    23 	ipairs = ipairs, load = load, loadfile = loadfile, loadstring = loadstring,
    24 	next = next, pairs = pairs, pcall = pcall, print = print,
    25 	rawequal = rawequal, rawget = rawget, rawset = rawset, require = require,
    26 	select = select, setfenv = setfenv, setmetatable = setmetatable,
    27 	tonumber = tonumber, tostring = tostring, type = type, unpack = unpack,
    28 	xpcall = xpcall
    29 }
    30 
    31 local table, string, assert, unpack, ipairs, pairs, type, require =
    32 	table, string, assert, unpack, ipairs, pairs, type, require
    33 local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv
    34 local open, remove, rename, getenv, time, date =
    35 	io.open, os.remove, os.rename, os.getenv, os.time, os.date
    36 
    37 
    38 module "loona"
    39 
    40 
    41 _VERSION = 3
    42 _REVISION = 1
    43 
    44 
    45 local function lookupname(tab, val)
    46 	if tab then
    47 		for i, v in ipairs(tab) do
    48 			if v.name == val then
    49 				return i
    50 			end
    51 		end
    52 	end
    53 end
    54 
    55 
    56 local atom = { }
    57 
    58 
    59 function atom:new(o)
    60 	o = o or { }
    61 	setmetatable(o, self)
    62 	self.__index = self
    63 	return o
    64 end
    65 
    66 
    67 local loona = atom:new(getfenv())
    68 
    69 
    70 function loona:dbmsg(msg, detail)
    71  	return (msg and detail and self.authuser) and
    72  		("%s : %s"):format(msg, detail) or msg
    73 end
    74 
    75 
    76 function loona:checkprofilename(n)
    77 	assert(n:match("^%w+$") and n ~= "current",
    78 		self:dbmsg("Invalid profile name", n))
    79 	return n
    80 end
    81 
    82 
    83 function loona:checkbodyname(s)
    84 	s = s or "main"
    85 	assert(s:match("^[%w_]*%w+[%w_]*$"), self:dbmsg("Invalid body name", s))
    86 	return s
    87 end
    88 
    89 
    90 function loona:deleteprofile(p, lang)
    91 	p = self.config.contentdir .. "/" .. p .. "_" .. (lang or self.lang)
    92 	for e in tek.util.readdir(p) do
    93  		local success, msg = remove(p .. "/" .. e)
    94 		assert(success, self:dbmsg("Error removing entry in profile", msg))
    95 	end
    96 	return remove(p)
    97 end
    98 
    99 
   100 function loona:copyprofile(newprofile, srcprofile, lang)
   101 	srcprofile = srcprofile or self.profile
   102 	lang = lang or self.lang
   103 	local contentdir = self.config.contentdir
   104 	local src = ("%s/%s_%s"):format(contentdir, srcprofile or self.profile, 
   105 		lang)
   106 	assert(posix.stat(src, "mode") == "directory",
   107 		self:dbmsg("Not a directory", src))
   108 	local dst = ("%s/%s_%s"):format(contentdir, newprofile, lang)
   109 	local success, msg = posix.mkdir(dst)
   110 	assert(success, self:dbmsg("Error creating profile directory", msg))
   111 	for e in tek.util.readdir(src) do
   112 		local ext = e:match("^[^.].*%.([^.]*)$")
   113 		if ext ~= "LOCK" then
   114 			local f = src .. "/" .. e
   115 			if posix.stat(f, "mode") == "file" then
   116 				success, msg = tek.copyfile(f, dst .. "/" .. e)
   117 				assert(success, self:dbmsg("Error copying file", msg))
   118 			end
   119 		end
   120 	end
   121 end
   122 
   123 
   124 function loona:publishprofile(profile, lang)
   125 	lang = lang or self.lang
   126 	local contentdir = self.config.contentdir
   127 	local newpath = ("%s/current_%s"):format(contentdir, lang)
   128 	local tmppath = newpath .. "." .. self.session.name
   129 	local success, msg = posix.symlink(profile .. "_" .. lang, tmppath)
   130 	assert(success, self:dbmsg("Cannot create symlink", msg))
   131 	success, msg = rename(tmppath, newpath)
   132 	assert(success, self:dbmsg("Cannot put symlink in place", msg))
   133 	
   134 	-- Get languages in the current profile
   135 	
   136 	local plangs = { }
   137 	local lmatch = "^" .. self.profile .. "_(%w+)$"
   138 	for e in tek.util.readdir(self.config.contentdir) do
   139 		local l = e:match(lmatch)
   140 		if l then
   141 			table.insert(plangs, l)
   142 		end
   143 	end
   144 	
   145 	-- For all languages, unroll site to static HTML
   146 	
   147 	for _, lang in ipairs(plangs) do
   148 		local ext = (#plangs == 1 and ".html") or (".html." .. lang)
   149 		self:recursesections(self.sections, function(self, s, e, path)
   150 			path = path and path .. "/" .. e.name or e.name
   151 			if not e.notvisible then
   152 				loona:dumphtml { requestpath = path, requestlang = lang,
   153 					htmlext = ext, insecure = true }
   154 			end
   155 			return path
   156 		end)
   157 	end
   158 	
   159 	-- Update file cache
   160 
   161 	local htdocs = self.config.htdocsdir
   162 	local cache = self.config.htmlcachedir
   163 
   164 	for e in tek.util.readdir(cache) do
   165 		local f = e:match("^.*%.html%.?(%w*)$")
   166 		if f and f ~= "tmp" then
   167 			local success, msg = remove(htdocs .. "/" .. e)
   168 			success, msg = remove(cache .. "/" .. e)
   169  			assert(success,
   170  				self:dbmsg("Could not purge cached HTML file", msg))
   171 		end
   172 	end
   173 	
   174 	for e in tek.util.readdir(cache) do
   175 		local f = e:match("^(.*%.html%.?%w*)%.tmp$")
   176 		if f then
   177 			local success, msg = rename(cache .. "/" .. e, cache .. "/" .. f)
   178 			assert(success,
   179 				self:dbmsg("Could not update cached HTML file", msg))
   180 			success, msg = rename(htdocs .. "/" .. e, htdocs .. "/" .. f)
   181 			assert(success,
   182 				self:dbmsg("Could not update cached HTML file", msg))
   183 		end
   184 	end
   185 end
   186 
   187 
   188 function loona:recursesections(s, func, ...)
   189 	for _, e in ipairs(s) do
   190 		local udata = { func(self, s, e, unpack(arg)) }
   191 		if e.subs then
   192 			self:recursesections(e.subs, func, unpack(udata))
   193 		end
   194 	end
   195 end
   196 
   197 
   198 function loona:indexsections()
   199 	self:recursesections(self.sections, function(self, s, e)
   200 		e.notvalid = (not self.secure and e.secure) or 
   201 			(not self.authuser and e.secret) or nil
   202 		e.notvisible = e.notvalid or not self.authuser and e.hidden or nil
   203 		s[e.name] = e
   204 	end)
   205 end
   206 
   207 
   208 --	Decompose section path into a stack of sections, returning only up to
   209 --	the last valid element in the path. additionally returns the table of
   210 --	the last section path element (or the default section)
   211 
   212 function loona:getsection(path)
   213 	local default = not self.authuser and self.config.defname
   214 	local tab = { { entries = self.sections, name = default } }
   215 	local ss = self.sections
   216 	local sectionpath
   217 	(path or ""):gsub("(%w+)/?", function(a)
   218 		if ss then
   219 			local s = ss[a]
   220 			if s and not s.notvalid then
   221 				sectionpath = s
   222 				tab[#tab].name = a
   223 				ss = s.subs
   224 				if ss then
   225 					table.insert(tab, { entries = ss })
   226 				end
   227 			else
   228 				ss = nil -- stop.
   229 			end
   230 		end
   231 	end)
   232 	if not self.section and not sectionpath then
   233 		sectionpath = self.sections[default]
   234 		if sectionpath then
   235 			table.insert(tab, { entries = sectionpath.subs })
   236 		end
   237 	end
   238 	return tab, sectionpath
   239 end
   240 
   241 
   242 function loona:getpath(delimiter)
   243 	local t = { }
   244 	for _, menu in ipairs(self.submenus) do
   245 		if menu.name then
   246 			table.insert(t, menu.name)
   247 		end
   248 	end
   249 	return table.concat(t, delimiter or "/")
   250 end
   251 
   252 
   253 function loona:deletesection(fname, all_bodies)
   254 	local fullname = self.contentdir .. "/" .. fname
   255 	local success, msg = remove(fullname)
   256 	if all_bodies then
   257 		local pat = "^" .. 
   258 			fname:gsub("%^%$%(%)%%%.%[%]%*%+%-%?", "%%%1") .. "%..*$"
   259 		for e in tek.util.readdir(self.contentdir) do
   260 			if e:match(pat) then
   261 				remove(self.contentdir .. "/" .. e)
   262 			end
   263 		end
   264 	end
   265 	return success, msg
   266 end
   267 
   268 
   269 function loona:addpath(path, e)
   270 	local tab = self.sections
   271 	path:gsub("(%w+)/?", function(a)
   272 		if tab then
   273 			local s = tab[a]
   274 			if s then
   275 				if not s.subs then
   276 					s.subs = { }
   277 				end
   278 				tab = s.subs
   279 			else
   280 				table.insert(tab, e)
   281 				tab[a] = e
   282  				tab = nil -- stop
   283 			end
   284 		end
   285 	end)
   286 	return e
   287 end
   288 
   289 
   290 function loona:rmpath(path)
   291 	local parent
   292 	local tab = self.sections
   293 	path:gsub("(%w+)/?", function(a)
   294 		if tab then
   295 			local idx = lookupname(tab, a)
   296 			if idx then
   297 				if tab[idx].subs then
   298 					parent = tab[idx]
   299 					tab = tab[idx].subs
   300 				else
   301 					table.remove(tab, idx)
   302 					tab[a] = nil
   303 					if #tab == 0 and parent then
   304 						parent.subs = nil
   305 					end
   306 					tab = nil
   307 				end
   308 			end
   309 		end
   310 	end)
   311 end
   312 
   313 
   314 function loona:checkpath(path)
   315 	if path ~= "index" then -- "index" is reserved
   316 		local res, idx
   317 		local tab = self.sections
   318 		path:gsub("(%w+)/?", function(a)
   319 			if tab then
   320 				local i = lookupname(tab, a)
   321 				if i then
   322 					res, idx = tab, i
   323 					tab = tab[i].subs
   324 				else
   325 					res, idx = nil, nil
   326 				end
   327 			end
   328 		end)
   329 		return res, idx
   330 	end
   331 end
   332 
   333 
   334 function loona:title()
   335 	return self.section and (self.section.title or self.section.label or
   336 		self.section.name) or ""
   337 end
   338 
   339 
   340 --	Run a site function snippet, with full error recovery
   341 --	(also recovers from errors in error handling function)
   342 
   343 function loona:dosnippet(func, errfunc)
   344 	local ret = { tek.catch(func) }
   345 	if ret[1] == 0 or (errfunc and tek.catch(errfunc) == 0) then
   346 		return unpack(ret)
   347 	end
   348 	self:out("<h2>Error</h2>")
   349 	self:out("<h3>" .. self:encodeform(ret[2] or "") .. "</h3>")
   350 	if self.authuser then
   351 		if type(ret[3]) == "string" then
   352 			self:out("<p>" .. self:encodeform(ret[3]) .. "</p>")
   353 		end
   354 		if ret[4] then
   355 			self:out("<pre>" .. self:encodeform(ret[4]) .. "</pre>")
   356 		end
   357 	end
   358 end	
   359 
   360 
   361 function loona:lockfile(file)
   362 	return not self.session and true or 
   363 		posix.symlink(self.session.filename, file .. ".LOCK")
   364 end
   365 
   366 
   367 function loona:unlockfile(file)
   368 	return not self.session and true or remove(file .. ".LOCK")
   369 end
   370 
   371 
   372 function loona:saveindex()
   373 	local tempname = self.indexfname .. "." .. self.session.name
   374 	local f, msg = open(tempname, "wb")
   375 	assert(f, self:dbmsg("Error opening section file for writing", msg))
   376 	tek.dump(self.sections, function(...)
   377 		f:write(unpack(arg))
   378 	end)
   379 	f:close()
   380 	local success, msg = rename(tempname, self.indexfname)
   381 	assert(success, self:dbmsg("Error renaming section file", msg))
   382 end
   383 
   384 
   385 function loona:savebody(fname, content)
   386 	fname = self.contentdir .. "/" .. fname
   387 	local f, msg = open(fname, "wb")
   388 	assert(f, self:dbmsg("Could not open file for writing", msg))
   389 	f:write(content or "")
   390 	f:close()
   391 end
   392 
   393 
   394 function loona:runboxed(func, envitems, ...)
   395 	local fenv = {
   396  		arg = arg,
   397  		loona = self
   398  	}
   399  	if envitems then
   400 	 	for k, v in pairs(envitems) do
   401  			fenv[k] = v
   402  		end
   403  	end
   404  	setmetatable(fenv, { __index = boxed_G }) -- boxed global environment
   405 	setfenv(func, fenv)
   406 	return func()
   407 end
   408 
   409 
   410 function loona:include(fname, ...)
   411 	assert(not fname:match("%W"), self:dbmsg("Invalid include name", fname))
   412 	local fname2 = ("%s/%s.lua"):format(self.config.extdir, fname)
   413 	local f, msg = open(fname2)
   414 	assert(f, self:dbmsg("Cannot open file", msg))
   415 	local parsed, msg = self:loadhtml(f, "loona:out", fname2)
   416 	assert(parsed, self:dbmsg("Syntax error", msg))
   417 	return self:runboxed(parsed)
   418 end
   419 
   420 
   421 --	produce link target, propagate lang, profile, session
   422 
   423 function loona:href(section, ...)
   424 	local target = self:getdocname(section)
   425 	if self.session then
   426 		return tek.web.gethref(target, { "profile", "session", "lang", 
   427 			unpack(arg) })
   428 	end
   429 	return tek.web.gethref(target, { "lang", unpack(arg) })
   430 end
   431 
   432 
   433 function loona:ilink(target, text, extra)
   434 	return ('<a href="%s"%s>%s</a>'):format(target, extra or "", text)
   435 end
   436 
   437 
   438 --	internal link, propagation of lang, profile, session
   439 
   440 function loona:link(section, text, ...)
   441 	return self:ilink(self:href(section, unpack(arg)), text or section)
   442 end
   443 
   444 
   445 --	external link (opens in a new window), no argument propagation
   446 
   447 function loona:elink(target, text)
   448 	return self:ilink(target, text or target,
   449 		not self.config.extlinksamewindow and
   450 		' class="extlink" onclick="void(window.open(this.href, \'\', \'\')); return false;"')
   451 end
   452 
   453 
   454 --	plain link, no argument propagation
   455 
   456 function loona:plink(section, text, ...)
   457 	return self:ilink(tek.web.gethref(section, arg), text or section)
   458 end
   459 
   460 
   461 --	user interface link, propagation of lang, profile, session
   462 
   463 function loona:uilink(section, text, ...)
   464 	return self:ilink(self:href(section, unpack(arg)), text or section)
   465 end
   466 
   467 
   468 --	produce a hidden input value in forms
   469 
   470 function loona:hidden(name, value)
   471 	return not value and "" or
   472 		('<input type="hidden" name="%s" value="%s" />'):format(name, value)
   473 end
   474 
   475 
   476 function loona:getprofiles(lang)
   477 	lang = lang or self.lang
   478 	local dir = self.config.contentdir
   479 	local t = { }
   480 	for f in tek.util.readdir(dir) do
   481 		if posix.lstat(dir .. "/" .. f, "mode") == "directory" then
   482 			local e = f:match("^(%w+)_" .. lang .. "$")
   483  			if e then
   484 	 			t[e] = e
   485  	 		end
   486 		end
   487 	end
   488 	return t
   489 end
   490 
   491 
   492 --	Functions to produce a hierarchical navigation menu
   493 
   494 local newent = { name = "new", label = "[+]", action="actionnew=true" }
   495 
   496 function loona:rmenu(level, linkf, path, addnew)
   497 	local sub = (addnew and level == #self.submenus + 1) and
   498 		{ name = "new", entries = { }} or self.submenus[level]
   499  	if sub and sub.entries then
   500 		local visible = { }
   501 		for _, e in ipairs(sub.entries) do
   502 			if not e.notvisible then
   503 				table.insert(visible, e)
   504 			end
   505 		end
   506 		if addnew then
   507 			table.insert(visible, newent)
   508 		end
   509 		if #visible > 0 then
   510 			self:out('<ul id="menulevel' .. level .. '">\n')
   511 			for _, e in ipairs(visible) do
   512 				local label = self:encodeform(e.label or e.name)
   513 				local newpath = path and path .. "/" .. e.name or e.name
   514 				local active = (e.name == sub.name)
   515 				self:out('<li>\n')
   516 				linkf(self, newpath, label, active, e.action)
   517 				if active then
   518 					self:rmenu(level + 1, linkf, newpath, addnew)
   519 				end
   520 				self:out('</li>\n')
   521 			end
   522 			self:out('</ul>\n')
   523 		end
   524 	end
   525 end
   526 
   527 
   528 function loona:menulink(path, label, active, ...)
   529 	self:out(('<a %shref="%s">%s</a>\n'):format(active and 
   530 		'class="active" ' or "", self:href(path, unpack(arg)), label))
   531 end
   532 
   533 
   534 function loona:menu(level, linkf)
   535 	local addnew = self.authuser and not self.ispubprofile
   536 	self:rmenu(level or 1, linkf or menulink, nil, addnew)
   537 end
   538 
   539 
   540 function loona:loadcontent(fname)
   541 	if fname then
   542 		local f = open(self.contentdir .. "/" .. fname)
   543 		local c = f:read("*a")
   544 		f:close()
   545 		return c
   546 	end
   547 	return ""
   548 end
   549 
   550 
   551 function loona:loadmarkup(fname)
   552 	return (fname and fname ~= "") and
   553 		self:domarkup(self:loadcontent(fname)) or ""
   554 end
   555 
   556 
   557 function loona:editable(editkey, fname, savename)
   558 	
   559 	local contentdir = self.contentdir
   560 	local edit, show, hidden, extramsg, changed
   561 	
   562 	if self.authuser then
   563 		
   564 		local hiddenvars = table.concat( {
   565 			self:hidden("lang", self.args.lang),
   566 			self:hidden("profile", self.profile),
   567 			self:hidden("session", self.session.id),
   568 			self:hidden("editkey", editkey) }, " ")
   569 	
   570 		local lockfname = fname and (contentdir .. "/" .. fname)
   571 		
   572 		if self.useralert and editkey == self.args.editkey then
   573 			
   574 			--	display user alert/request/confirmation
   575 			
   576 			hidden = true
   577 			self:out([[
   578 			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   579 				<fieldset>
   580 					<legend>]] .. self.useralert.text ..[[</legend>
   581 					]] .. (self.useralert.confirm or "") .. [[
   582 					<input type="submit" name="actioncancel" value="]] .. self.locale.CANCEL  ..[[" />
   583 					]] .. hiddenvars .. [[
   584 				</fieldset>
   585 			</form>
   586 			]])
   587 			
   588 		elseif self.args.actionnew and editkey == "main" then
   589 			
   590 			--	form for creating a new section
   591 			
   592 			hidden = true
   593 			if self.ispubprofile then
   594 				self:out([[<h2><span class="warn">]] .. self.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE ..[[</span></h2>]])
   595 			end
   596 			self:out([[
   597 			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   598 				<fieldset>
   599 					<legend>
   600 						]] .. self.locale.CREATE_NEW_SECTION_UNDER .. " " .. self.sectionpath .. [[
   601 					</legend>
   602 					<table>
   603 						<tr>
   604 							<td align="right">
   605 								]] .. self.locale.PATHNAME .. [[
   606 							</td>
   607 							<td>
   608 								<input size="30" maxlength="30" name="editname" />
   609 							</td>
   610 						</tr>
   611 						<tr>
   612 							<td align="right">
   613 								]] .. self.locale.MENULABEL .. [[
   614 							</td>
   615 							<td>
   616 								<input size="30" maxlength="50" name="editlabel" />
   617 							</td>
   618 						</tr>
   619 						<tr>
   620 							<td align="right">
   621 								]] .. self.locale.WINDOWTITLE .. [[
   622 							</td>
   623 							<td>
   624 								<input size="30" maxlength="50" name="edittitle" />
   625 							</td>
   626 						</tr>
   627 						<tr>
   628 							<td align="right">
   629 								]] .. self.locale.INVISIBLE .. [[
   630 							</td>
   631 							<td>
   632 								<input type="checkbox" name="editvisibility" />
   633 							</td>
   634 						</tr>
   635 						<tr>
   636 							<td align="right">
   637 								]] .. self.locale.SECRET .. [[
   638 							</td>
   639 							<td>
   640 								<input type="checkbox" name="editsecrecy" />
   641 							</td>
   642 						</tr>
   643 						<tr>
   644 							<td align="right">
   645 								]] .. self.locale.SECURE_CONNECTION .. [[
   646 							</td>
   647 							<td>
   648 								<input type="checkbox" name="editsecure" />
   649 							</td>
   650 						</tr>
   651 						<tr>
   652 							<td align="right">
   653 								]] .. self.locale.REDIRECT .. [[
   654 							</td>
   655 							<td>
   656 								<input size="30" maxlength="50" name="editredirect" />
   657 							</td>
   658 						</tr>
   659 					</table>					
   660 					<input type="submit" name="actioncreate" value="]] .. self.locale.CREATE .. [[" />
   661 					]] .. hiddenvars .. [[
   662 				</fieldset>
   663 			</form>
   664 			<hr />
   665 			]])
   666 		
   667 		elseif self.args.actioneditprops and editkey == "main" then
   668 			hidden = true
   669 			if self.ispubprofile then
   670 				self:out([[<h2><span class="warn">]] .. self.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE ..[[</span></h2>]])
   671 			end
   672 			self:out([[
   673 			<form action="]] ..self.document .. [[" method="post" accept-charset="utf-8">
   674 				<fieldset>
   675 					<legend>
   676 						]] .. self.locale.MODIFY_PROPERTIES_OF_SECTION .. " " .. self.sectionpath .. [[
   677 					</legend>
   678 					<table>
   679 						<tr>
   680 							<td align="right">
   681 								]] .. self.locale.MENULABEL .. [[
   682 							</td>
   683 							<td>
   684 								<input size="30" maxlength="50" name="editlabel" value="]] .. (self.section.label or "") .. [[" />
   685 							</td>
   686 						</tr>
   687 						<tr>
   688 							<td align="right">
   689 								]] .. self.locale.WINDOWTITLE .. [[
   690 							</td>
   691 							<td>
   692 								<input size="30" maxlength="50" name="edittitle" value="]] .. (self.section.title or "") .. [[" />
   693 							</td>
   694 						</tr>
   695 						<tr>
   696 							<td align="right">
   697 								]] .. self.locale.INVISIBLE .. [[
   698 							</td>
   699 							<td>
   700 								<input type="checkbox" name="editvisibility" ]] .. (self.section.hidden and 'checked="checked"' or "") .. [[/>
   701 							</td>
   702 						</tr>
   703 						<tr>
   704 							<td align="right">
   705 								]] .. self.locale.SECRET .. [[
   706 							</td>
   707 							<td>
   708 								<input type="checkbox" name="editsecrecy" ]] .. (self.section.secret and 'checked="checked"' or "") .. [[/>
   709 							</td>
   710 						</tr>
   711 						<tr>
   712 							<td align="right">
   713 								]] .. self.locale.SECURE_CONNECTION .. [[
   714 							</td>
   715 							<td>
   716 								<input type="checkbox" name="editsecure" ]] .. (self.section.secure and 'checked="checked"' or "") .. [[/>
   717 							</td>
   718 						</tr>
   719 						<tr>
   720 							<td align="right">
   721 								]] .. self.locale.REDIRECT .. [[
   722 							</td>
   723 							<td>
   724 								<input size="30" maxlength="50" name="editredirect" value="]] .. (self.section.redirect or "") .. [[" />
   725 							</td>
   726 						</tr>
   727 					</table>
   728 					<input type="submit" name="actionsaveprops" value="]] .. self.locale.SAVE .. [[" />
   729 					<input type="submit" name="actioncancel" value="]] .. self.locale.CANCEL .. [[" />
   730 					]] .. hiddenvars .. [[
   731 				</fieldset>
   732 			</form>
   733 			]])
   734 		
   735 		elseif (self.args.actioneditprofiles or
   736 			self.args.actioncreateprofile or 
   737 			self.args.actionchangeprofile or 
   738 			self.args.actionpublishprofile) and editkey == "main" then
   739 			hidden = true
   740 			local profiles = { }
   741 			for p in pairs(self:getprofiles()) do
   742 				table.insert(profiles, p)
   743 			end
   744 			table.sort(profiles)
   745 			self:out([[
   746 			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   747 				<fieldset>
   748 					<legend>
   749 						]] .. self.locale.CHANGEPROFILE .. [[
   750 					</legend>
   751 					<select name="changeprofile" size="1">]])
   752 						for _, val in ipairs(profiles) do
   753 							self:out('<option' .. (val == self.profile and " selected" or "") .. '>')
   754 							self:out(val)
   755 							self:out('</option>')
   756 						end
   757 					self:out([[
   758 					</select>							
   759 					<input type="submit" name="actionchangeprofile" value="]] .. self.locale.CHANGE ..[[" />
   760 					]] .. hiddenvars .. [[
   761 				</fieldset>
   762 			</form>
   763 			<form action="]] .. self.document ..[[" method="post" accept-charset="utf-8">
   764 				<fieldset>
   765 					<legend>
   766 						]] .. self.locale.CREATEPROFILE .. [[
   767 					</legend>
   768 					<input size="20" maxlength="20" name="createprofile" />
   769 					<input type="submit" name="actioncreateprofile" value="]] .. self.locale.CREATE .. [[" />
   770 					]] .. hiddenvars .. [[
   771 				</fieldset>
   772 			</form>
   773 			]])
   774 			if not self.ispubprofile then
   775 				self:out([[
   776 				<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   777 					<fieldset>
   778 						<legend>
   779 							]] .. self.locale.PUBLISHPROFILE .. [[
   780 						</legend>
   781 						]] .. self:hidden("publishprofile", self.profile) .. [[
   782 						<input type="submit" name="actionpublishprofile" value="]] .. self.locale.PUBLISH .. [[" />
   783 						]] .. hiddenvars .. [[
   784 					</fieldset>
   785 				</form>
   786 				<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   787 					<fieldset>
   788 						<legend>
   789 							]] .. self.locale.DELETEPROFILE .. [[
   790 						</legend>
   791 						]] .. self:hidden("deleteprofile", self.profile) .. [[
   792 						<input type="submit" name="actiondeleteprofile" value="]] .. self.locale.DELETE .. [[" />
   793 						]] .. hiddenvars .. [[
   794 					</fieldset>
   795 				</form>
   796 				]])
   797 			end
   798 			
   799 		elseif self.args.actionedit and editkey == self.args.editkey then
   800 			if not self.section.redirect then
   801 				extramsg = self.ispubprofile and
   802 					self.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE
   803 				edit = self:loadcontent(fname):gsub("\194\160", "&nbsp;") -- TODO
   804 				changed = self.section and (self.section.revisiondate or self.section.creationdate)
   805 			end
   806 		
   807 		elseif self.args.actionpreview and editkey == self.args.editkey then
   808 			edit = self.args.editform
   809 			show = self:domarkup(edit:gsub("&nbsp;", "\194\160")) -- TODO
   810 		
   811 		elseif self.args.actionsave and editkey == self.args.editkey then
   812 			local c = self.args.editform
   813 			local dynamic
   814 			
   815 			if lockfname then
   816 				self:expire(contentdir, "[^.]%S+.LOCK")
   817 				if self:lockfile(lockfname) then
   818 					-- lock was expired, aquired a new one
   819 					extramsg = self.locale.SECTION_COULD_HAVE_CHANGED
   820 					edit = c
   821 				else
   822 					local tab = tek.source(lockfname .. ".LOCK")
   823 					if tab and tab.id == self.session.id then
   824 						-- lock already held and is mine - try to save:
   825 						local savec = c:gsub("&nbsp;", "\194\160") -- TODO
   826 						remove(contentdir .. "/" .. savename .. ".html")
   827 						self:savebody(savename, savec)
   828 						-- TODO: error handling
   829 						self:unlockfile(lockfname)
   830 						show, dynamic = self:domarkup(savec)
   831 						changed = time()
   832 					else
   833 						-- lock was expired and someone else has it now
   834 						extramsg = self.locale.SECTION_IN_USE
   835 						edit = c
   836 					end
   837 				end
   838 			else
   839 				-- new sidefile
   840 				local savec = c:gsub("&nbsp;", "\194\160") -- TODO
   841 				self:savebody(savename, savec)
   842 				-- TODO: error handling
   843 				show, dynamic = self:domarkup(savec)
   844 			end
   845 			
   846 			-- mark dynamic text bodies
   847 			if not self.section.dynamic then
   848 				self.section.dynamic = { }
   849 			end
   850 			self.section.dynamic[editkey] = dynamic
   851 			local n = 0
   852 			for _ in pairs(self.section.dynamic) do
   853 				n = n + 1
   854 			end
   855 			if n == 0 then
   856 				self.section.dynamic = nil
   857 			end
   858 			
   859 			self:saveindex()
   860 			
   861 		elseif self.args.actioncancel and editkey == self.args.editkey then
   862 			if lockfname then
   863 				self:unlockfile(lockfname) -- remove lock
   864 			end
   865 		end
   866 		
   867 		if editkey == "main" and self.section and self.section.redirect then
   868 			self:out('<h2>' .. self.locale.SECTION_IS_REDIRECT ..'</h2>')
   869 			self:out(self:link(self.section.redirect))
   870 			self:out('<hr />')
   871 		end
   872 	
   873 		if edit then
   874 			self:expire(contentdir, "[^.]%S+.LOCK")
   875 			if fname and not self:lockfile(contentdir .. "/" .. fname) then
   876 				local tab = tek.source(contentdir .. "/" .. fname .. ".LOCK")
   877 				if tab and tab.id ~= self.session.id then
   878 					extramsg = self.locale.SECTION_IN_USE
   879 				end
   880 				-- else already owner
   881 			end
   882 			if extramsg then
   883 				self:out('<h2><span class="warn">' .. extramsg .. '</span></h2>')
   884 			end
   885 			self:out([[
   886 			<form action="]] .. self.document .. [[#preview" method="post" accept-charset="utf-8">
   887 				<fieldset>
   888 					<legend>
   889 						]] .. self.locale.EDIT_SECTION .. [[
   890 					</legend>
   891 					<textarea cols="80" rows="25" name="editform">]] .. self:encodeform(edit) .. [[</textarea>
   892 					<br />
   893 					<input type="submit" name="actionsave" value="]] .. self.locale.SAVE .. [[" />
   894 					<input type="submit" name="actionpreview" value="]] .. self.locale.PREVIEW .. [[" />
   895 					<input type="submit" name="actioncancel" value="]] .. self.locale.CANCEL .. [[" />
   896 					]] .. hiddenvars .. [[
   897 				</fieldset>
   898 			</form>
   899 			]])
   900 		end
   901 	end	
   902 	
   903 	if not hidden then
   904 		self:dosnippet(function()
   905 			if not show then
   906 				show = self:loadmarkup(fname)
   907 				changed = self.section and (self.section.revisiondate or self.section.creationdate)
   908 			end
   909 			local parsed, msg = self:loadhtml(show, "loona:out", "<parsed html>")
   910 			assert(parsed, msg and "Syntax error : " .. msg)
   911 			self:runboxed(parsed)
   912 		end)
   913 	end
   914 	
   915 	if self.authuser then
   916 		self:out([[
   917 		<hr />
   918 		<div class="edit">]])
   919 			if editkey == "main" then
   920 				self:out([[
   921 				<a name="preview"></a>
   922 				]] .. self.authuser .. [[ : 
   923 				]] .. self:uilink(self.sectionpath, "[" .. self.locale.PROFILE .. "]", "actioneditprofiles=true", "editkey=" .. editkey) .. [[ :
   924 				]] .. self.profile .. "_" .. self.lang)
   925 				if self.ispubprofile then
   926 					self:out([[
   927 						<span class="warn">[]] .. self.locale.PUBLIC .. [[]</span>]])
   928 				end
   929 				self:out(' : ' .. self.sectionpath .. ' ')
   930 			end
   931 			if self.section and not self.ispubprofile then
   932 				self:out(self:uilink(self.sectionpath, "[" .. self.locale.EDIT .. "]", "actionedit=true", "editkey=" .. editkey) .. " ")
   933 				if editkey == "main" then
   934 					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.PROPERTIES .. "]", "actioneditprops=true", "editkey=" .. editkey) .. " ")
   935 				end
   936 				if fname == savename or not self.section.subs then
   937 					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.DELETE .. "]", "actiondelete=true", "editkey=" .. editkey) .. " ")
   938 				end
   939 				if editkey == "main" then
   940 					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.MOVEUP .. "]", "actionup=true", "editkey=" .. editkey) .. " ")
   941 					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.MOVEDOWN .. "]", "actiondown=true", "editkey=" .. editkey) .. " ")
   942 				end
   943 				if changed and editkey == "main" then
   944 					self:out('- ' .. self.locale.CHANGED .. ': ' .. date("%d-%b-%Y %T", changed))
   945 				end
   946 			end
   947 		self:out('</div>')
   948 	end
   949 
   950 end
   951 
   952 
   953 --	Get pathname of an existing content file that
   954 --	the current path is determined by (or defaults to)
   955 
   956 function loona:getsectionpath(bodyname, requestpath)
   957 	local ext = (not bodyname or bodyname == "main") and "" or "." .. bodyname
   958 	local t, path, section = { }
   959 	for _, menu in ipairs(self.submenus) do
   960 		if menu.entries and menu.entries[menu.name] then
   961 			table.insert(t, menu.name)
   962 			local fn = table.concat(t, "_")
   963 			if posix.stat(self.contentdir .. "/" .. fn .. ext, 
   964 				"mode") == "file" then
   965 				path, section = fn, menu
   966 			end
   967 		end
   968 	end
   969 	return path, ext, section
   970 end
   971 
   972 
   973 function loona:body(name)
   974 	name = self:checkbodyname(name)
   975 	local path, ext = self:getsectionpath(name)
   976 -- 	self:dosnippet(self:editable(name, path and path .. ext, self.sectionname .. ext))
   977 	self:dosnippet(function()
   978 		self:editable(name, path and path .. ext, self.sectionname .. ext)
   979 	end)
   980 end
   981 
   982 
   983 function loona:init()
   984 	
   985 	-- get list of languages, in order of preference
   986 	-- TODO: respect quality parameter, not just order
   987 	
   988 	local l = self.requestlang or self.args.lang
   989 	self.langs = { l and l:match("^%w+$") }
   990 	if self.config.browserlang then
   991 		local s = getenv("HTTP_ACCEPT_LANGUAGE")
   992 		while s do
   993 			local l, r = s:match("^([%w.=]+)[,;](.*)$")
   994 			l = l or s
   995 			s = r
   996 			if l:match("^%w+$") then
   997 				table.insert(self.langs, l)
   998 			end
   999 		end
  1000 	end
  1001 	table.insert(self.langs, self.config.deflang)
  1002 	
  1003 	-- get list of possible profiles
  1004 	
  1005 	local profiles = { }
  1006 	for e in tek.util.readdir(self.config.contentdir) do
  1007 		profiles[e] = e
  1008 	end
  1009 	
  1010 	-- get pubprofile
  1011 	
  1012 	for _, lang in ipairs(self.langs) do
  1013 		local p = posix.readlink(self.config.contentdir .. "/current_" .. lang)
  1014 		p = p and p:match("^(%w+)_" .. lang .. "$")
  1015 		if p then
  1016 			self.pubprofile = p
  1017 			break
  1018 		end
  1019 	end
  1020 	
  1021 	-- get profile
  1022 	
  1023 	local checkprofile =
  1024 		self.authuser and self.args.profile or self.pubprofile or "work"
  1025 	for _, lang in ipairs(self.langs) do
  1026 		if profiles[checkprofile .. "_" .. lang] then
  1027 			self.profile = checkprofile
  1028 			self.lang = lang
  1029 			break
  1030 		end
  1031 	end
  1032 	
  1033 	assert(self.profile and self.lang, "Invalid profile or language")
  1034 	
  1035 	
  1036 	self.ispubprofile = self.profile == self.pubprofile
  1037 	
  1038 	-- write back language and profile
  1039 
  1040 	self.args.lang = self.lang ~= self.config.deflang and self.lang or nil
  1041 	self.args.profile = self.profile
  1042 	
  1043 	
  1044 	-- determine content directory pathname and section filename
  1045 	
  1046 	self.contentdir =
  1047 		("%s/%s_%s"):format(self.config.contentdir, self.profile, self.lang)
  1048  	self.indexfname = self.contentdir .. "/.sections"
  1049 	
  1050 	-- load sections
  1051 	
  1052  	self.sections = tek.source(self.indexfname)
  1053 	
  1054 	-- index sections, determine visibility in menu
  1055 	
  1056 	self:indexsections()
  1057 	
  1058 	-- decompose request path, produce a stack of sections
  1059 	
  1060 	self.submenus, self.section = self:getsection(self.requestpath)
  1061 
  1062 	-- handle redirects if not logged on
  1063 	
  1064 	if not self.authuser and self.section and self.section.redirect then
  1065 		self.submenus, self.section = self:getsection(self.section.redirect)
  1066 	end
  1067 			
  1068 	-- section path and document name (refined)
  1069 	
  1070 	self.sectionpath = self:getpath()
  1071 	self.sectionname = self:getpath("_")
  1072 
  1073 end
  1074 
  1075 
  1076 function loona:handlechanges()
  1077 	
  1078 	local save
  1079 
  1080 	if self.args.editkey == "main" then
  1081 		
  1082 		-- In main editable section:
  1083 		
  1084 		if self.args.actioncreate then
  1085 			
  1086 			-- Create new section
  1087 			
  1088 			local editname = self.args.editname:lower()
  1089 			assert(not editname:match("%W"),
  1090 				dbmsg("Invalid section name", editname))
  1091 			if not (section and (section.subs or section)[editname]) then
  1092 				local newpath = (self.sectionpath and 
  1093 					(self.sectionpath .. "/")) .. editname
  1094 				local s = self:addpath(newpath, { name = editname,
  1095 					label = self.args.editlabel ~= "" and
  1096 						self.args.editlabel or nil,
  1097 					title = self.args.edittitle ~= "" and
  1098 						self.args.edittitle or nil,
  1099 					redirect = self.args.editredirect ~= "" and
  1100 						self.args.editredirect or nil,
  1101 					hidden = self.args.editvisibility and true,
  1102 					secret = self.args.editsecrecy and true,
  1103 					secure = self.args.editsecure and true,
  1104 					creator = self.authuser,
  1105 					creationdate = time() })
  1106 				save = true
  1107 			end
  1108 		
  1109 		elseif self.args.actionsave then
  1110 			
  1111 			-- Save section
  1112 			
  1113 			self.section.revisiondate = time()
  1114 			self.section.revisioner = self.authuser
  1115 			save = true
  1116  		
  1117 		elseif self.args.actionsaveprops then
  1118 			
  1119 			-- Save properties
  1120 			
  1121 			self.section.hidden = self.args.editvisibility and true
  1122 			self.section.secret = self.args.editsecrecy and true
  1123 			self.section.secure = self.args.editsecure and true
  1124 			self.section.label = self.args.editlabel ~= "" and
  1125 				self.args.editlabel or nil
  1126 			self.section.title = self.args.edittitle ~= "" and
  1127 				self.args.edittitle or nil
  1128 			self.section.redirect =
  1129 				self.args.editredirect ~= "" and self.args.editredirect or nil
  1130 			save = true
  1131 		
  1132 		elseif self.args.actionup then
  1133 			
  1134 			-- Move section up
  1135 			
  1136 			local t, i = self:checkpath(self.sectionpath)
  1137 			if t and i > 1 then
  1138 				if self.ispubprofile and not self.args.actionconfirm then
  1139 					useralert = {
  1140 						text = self.locale.ALERT_MOVE_IN_PUBLISHED_PROFILE,
  1141 						confirm =
  1142 							'<input type="submit" name="actionup" value="' ..
  1143 							self.locale.MOVE .. '" /> ' ..
  1144 							self:hidden("actionconfirm", "true")
  1145 					}
  1146 				else
  1147 					local item = table.remove(t, i)
  1148 					table.insert(t, i - 1, item)
  1149 					save = true
  1150 				end
  1151 			end
  1152 		
  1153 		elseif self.args.actiondown then
  1154 			
  1155 			-- Move section down
  1156 			
  1157 			local t, i = self:checkpath(self.sectionpath)
  1158 			if t and i < #t then
  1159 				if self.ispubprofile and not self.args.actionconfirm then
  1160 					useralert = {
  1161 						text = self.locale.ALERT_MOVE_IN_PUBLISHED_PROFILE,
  1162 						confirm =
  1163 							'<input type="submit" name="actiondown" value="' ..
  1164 							self.locale.MOVE .. '" /> ' ..
  1165 							self:hidden("actionconfirm", "true")
  1166 					}
  1167 				else
  1168 					local item = table.remove(t, i)
  1169 					table.insert(t, i + 1, item)
  1170 					save = true
  1171 				end
  1172 			end
  1173 		
  1174 		elseif self.args.actioncreateprofile and self.args.createprofile then
  1175 			
  1176 			-- Create profile
  1177 			
  1178 			local c = self:checkprofilename(self.args.createprofile:lower())
  1179 			if c == profile then
  1180 				useralert = { 
  1181 					text = self.locale.ALERT_CANNOT_COPY_PROFILE_TO_SELF
  1182 				}
  1183 			else
  1184 				local profiles = self:getprofiles()
  1185 				if profiles[c] and not self.args.actionconfirm then
  1186 					useralert = {
  1187 						text = c == self.pubprofile and 
  1188 							self.locale.ALERT_OVERWRITE_PUBLISHED_PROFILE or
  1189 							self.locale.ALERT_OVERWRITE_EXISTING_PROFILE,
  1190 						confirm = '<input type="submit" ' ..
  1191 							'name="actioncreateprofile" value="' ..
  1192 							self.locale.OVERWRITE .. '" /> ' ..
  1193 							self:hidden("actionconfirm", "true") ..
  1194 							self:hidden("createprofile", c)
  1195 					}
  1196 				else
  1197 					if profiles[c] then
  1198 						self:deleteprofile(c)
  1199 					end
  1200 					self:copyprofile(c)
  1201 				end
  1202 			end
  1203 		
  1204 		elseif self.args.actiondeleteprofile and self.args.deleteprofile then
  1205 			
  1206 			-- Delete profile
  1207 			
  1208 			local c = self:checkprofilename(self.args.deleteprofile:lower())
  1209 			assert(c ~= self.pubprofile,
  1210 				self:dbmsg("Cannot delete published profile", c))
  1211 			if self.args.actionconfirm then
  1212 				self:deleteprofile(c)
  1213 				self.profile = nil
  1214 				self.args.profile = nil
  1215 				self:init()
  1216 				save = true
  1217 			else
  1218 				useralert = { 
  1219 					text = self.locale.ALERT_DELETE_PROFILE,
  1220 					confirm = '<input type="submit" ' ..
  1221 						'name="actiondeleteprofile" value="' .. 
  1222 						self.locale.DELETE .. '" /> ' ..
  1223 						self:hidden("actionconfirm", "true") ..
  1224 						self:hidden("deleteprofile", c)
  1225 				}
  1226 			end
  1227 		
  1228 		elseif self.args.actionchangeprofile and self.args.changeprofile then
  1229 			
  1230 			-- Change profile
  1231 			
  1232 			local c = self:checkprofilename(self.args.changeprofile:lower())
  1233 			self.profile = c
  1234 			self.args.profile = c
  1235 			save = true
  1236 		
  1237 		elseif self.args.actionpublishprofile and self.args.publishprofile then
  1238 			
  1239 			-- Publish profile
  1240 			
  1241 			local c = self:checkprofilename(self.args.publishprofile:lower())
  1242 			if c ~= self.publicprofile then
  1243 				if self.args.actionconfirm then
  1244 					self:publishprofile(c)
  1245 					save = true
  1246 				else
  1247 					useralert = {
  1248 						text = self.locale.ALERT_PUBLISH_PROFILE,
  1249 						confirm = '<input type="submit" ' ..
  1250 							'name="actionpublishprofile" value="' ..
  1251 							self.locale.PUBLISH .. '" /> ' ..
  1252 							self:hidden("actionconfirm", "true") ..
  1253 							self:hidden("publishprofile", c)
  1254 					}
  1255 				end
  1256 			end
  1257 		end
  1258 		
  1259 	end
  1260 	
  1261 	if self.args.actiondelete then
  1262 		
  1263 		-- Delete section
  1264 		
  1265 		if not self.args.actionconfirm then
  1266 			useralert = {
  1267 				text = self.ispubprofile and
  1268 					self.locale.ALERT_DELETE_IN_PUBLISHED_PROFILE or
  1269 					self.locale.ALERT_DELETE_SECTION,
  1270 				confirm =
  1271 					'<input type="submit" name="actiondelete" value="' .. 
  1272 					self.locale.DELETE .. '" /> ' ..
  1273 					self:hidden("actionconfirm", "true")
  1274 			}
  1275 		else
  1276 			local key = self.args.editkey
  1277 			if key == "main" and not self.section.subs then
  1278 				self:deletesection(self.sectionname, true) -- all bodies
  1279 				self:rmpath(self.sectionpath) -- and node
  1280 			else
  1281 				local ext = (key == "main" and "") or "." .. key
  1282 				self:deletesection(self.sectionname .. ext) -- only text
  1283 				if self.section.dynamic then
  1284 					self.section.dynamic[key] = nil
  1285 					local n = 0
  1286 					for _ in pairs(self.section.dynamic) do
  1287 						n = n + 1
  1288 					end
  1289 					if n == 0 then
  1290 						self.section.dynamic = nil
  1291 					end
  1292 				end
  1293 			end
  1294 			save = true
  1295 		end
  1296 	end
  1297 		
  1298 	if save then
  1299 		self:saveindex()
  1300 		self:init()
  1301 	end
  1302 	
  1303 end
  1304 
  1305 
  1306 function loona:encodeform(s)
  1307 	return cgi.encodeform(s)
  1308 end
  1309 
  1310 
  1311 function loona:loadhtml(src, outfunc, chunkname)
  1312 	return tek.web.include.load(src, outfunc, chunkname)
  1313 end
  1314 
  1315 
  1316 function loona:domarkup(s)
  1317 	return tek.web.markup.main(s)
  1318 end
  1319 
  1320 
  1321 function loona:expire(dir, pat, maxage)
  1322 	return tek.util.expire(dir, pat, maxage)
  1323 end
  1324 
  1325 
  1326 function loona:new(o)
  1327 
  1328 	local parsed, msg
  1329 	
  1330 	o = o or { }
  1331 	o = atom.new(self, o)
  1332 	
  1333 	o.out = o.out or function(self, s) tek.web.out(s) end
  1334 	o.setheader = o.setheader or function(self, s) tek.web.setheader(s) end
  1335 	
  1336 	-- Get configuration
  1337 	
  1338 	o.config = o.config or tek.source(o.conffile or "../etc/config.lua") or { }
  1339 	o.config.defname = o.config.defname or "home"
  1340 	o.config.deflang = o.config.deflang or "en"
  1341 	o.config.sessionmaxage = o.config.sessionmaxage or 600
  1342 	o.config.secureport = o.config.secureport or 443
  1343 	o.config.passwdfile =
  1344 		posix.abspath(o.config.passwdfile or "../etc/passwd.lua")
  1345 	o.config.sessiondir =
  1346 		posix.abspath(o.config.sessiondir or "../var/sessions")
  1347 	o.config.extdir = posix.abspath(o.config.extdir or "../extensions")
  1348 	o.config.contentdir = posix.abspath(o.config.contentdir or "../content")
  1349 	o.config.localedir = posix.abspath(o.config.localedir or "../locale")
  1350 	o.config.htdocsdir = posix.abspath(o.config.htdocsdir or "../htdocs")
  1351 	o.config.htmlcachedir = 
  1352 		posix.abspath(o.config.htmlcachedir or "../var/htmlcache")
  1353 	
  1354 	-- Create proxy for on-demand loading of locales
  1355 	
  1356 	o.locale = { }
  1357 	local locmt = { }
  1358 	locmt.__index = function(_, key)
  1359 		for _, l in ipairs(o.langs) do
  1360 			locmt.__locale = tek.source(o.config.localedir .. "/" .. l)
  1361 			if locmt.__locale then
  1362 				break
  1363 			end
  1364 		end
  1365 		locmt.__index = function(tab, key)
  1366 			return locmt.__locale[key] or key
  1367 		end
  1368 		return locmt.__locale[key] or key
  1369 	end
  1370 	setmetatable(o.locale, locmt)
  1371 	
  1372 	-- Get request, args, document, script name, request path
  1373 	
  1374 	o.request = o.request or cgi.request
  1375 	o.args = o.args or cgi.request.args
  1376 	o.session = o.session or cgi.session
  1377  	
  1378  	o.scriptpath = o.scriptpath or cgi.document.Path
  1379 	o.requesthandler = o.requesthandler or cgi.document.Handler
  1380  	o.requestdocument = o.requestdocument or cgi.document.Name
  1381 	o.requestpath = o.requestpath or cgi.document.VirtualPath
  1382 
  1383 	-- Manage login and establish session
  1384 	
  1385 	o.session.init(o.config.sessiondir, o.args.session, o.config.sessionmaxage)
  1386 	if o.args.login then
  1387 		if o.args.login == "false" then
  1388 			o.session.delete()
  1389 			o.session = nil
  1390 		elseif o.args.password then
  1391 			o.loginfailed = true
  1392 			local pwddb = tek.source(o.config.passwdfile)
  1393 			local pwdentry = pwddb[o.args.login]
  1394 			if pwdentry and pwdentry.password == o.args.password then
  1395 				o.session.data.authuser = pwdentry.username
  1396 				o.session.data.id = o.session.id
  1397 				o.loginfailed = nil
  1398 			end
  1399 		end
  1400 	end
  1401 	
  1402 	o.secure = not o.insecure and (o.request.Port == o.config.secureport)
  1403 	o.authuser = o.session and o.session.data.authuser
  1404 	
  1405 	if o.nologin or not o.authuser then
  1406 		o.authuser = nil
  1407 		o.session = nil
  1408 	end
  1409 	
  1410 	-- Get lang, locale, profile, section
  1411 	
  1412 	o:init()
  1413 	if o.authuser then
  1414 		o:handlechanges()
  1415 	end
  1416 	
  1417 	-- Current document
  1418 	
  1419 	o.document = o.requestdocument .. "/" .. o.sectionpath
  1420 	if o.authuser then
  1421 		o.getdocname = function(self, path)
  1422 			return path and self.requestdocument .. "/" .. path or
  1423 				self.requestdocument
  1424 		end
  1425 	else
  1426 		o.getdocname = function(self, path)
  1427 			local dyn
  1428 			path = path or self.config.defname
  1429 			dyn, path = self:isdynamic(path)
  1430 			if dyn then
  1431 				return self.requestdocument .. "/" .. path
  1432 			end
  1433 			path = path == self.config.defname and "index" or path
  1434 			return "/" .. path:gsub("/", "_") .. ".html"
  1435 		end
  1436 	end
  1437 	
  1438 	-- Save session state
  1439 	
  1440 	if o.session then
  1441 		o.session.save()
  1442 	end
  1443 
  1444 	return o
  1445 end
  1446 
  1447 
  1448 function loona:execute(fname)
  1449 	self:indexdynamic()
  1450 	fname = fname or self.requesthandler
  1451 	local parsed, msg = self:loadhtml(open(fname), "loona:out", fname)
  1452 	assert(parsed, self:dbmsg("HTML/Lua parsing failed", msg))
  1453 	self:runboxed(parsed)
  1454 	return self
  1455 end
  1456 
  1457 
  1458 function loona:indexdynamic()
  1459 	self:recursesections(self.sections, function(self, s, e, path, dynamic)
  1460 		path = path and path .. "_" .. e.name or e.name
  1461 		dynamic = dynamic or { }
  1462 		for k in pairs(e.dynamic or { }) do
  1463 			dynamic[k] = true
  1464 		end
  1465 		for k in pairs(dynamic) do
  1466 			local ext = (k == "main" and "") or "." .. k
  1467 			if posix.stat(self.contentdir .. "/" .. path .. ext,
  1468 				"mode") == "file" then
  1469 				dynamic[k] = e.dynamic and e.dynamic[k]
  1470 			end
  1471 		end
  1472 		local n = 0
  1473 		for k in pairs(dynamic) do
  1474 			n = n + 1
  1475 		end
  1476 		if n > 0 then
  1477 			e.dynamic = { }
  1478 			for k in pairs(dynamic) do
  1479 				e.dynamic[k] = true
  1480 			end
  1481 		else
  1482 			e.dynamic = nil
  1483 		end
  1484 		return path, dynamic
  1485 	end)
  1486 end
  1487 
  1488 
  1489 function loona:isdynamic(path)
  1490 	path = path or self.sectionpath
  1491 	local t, i = self:checkpath(path)
  1492 	if t and t[i].redirect then
  1493 		path = t[i].redirect
  1494 		t, i = self:isdynamic(path) -- TODO: prohibit endless recursion
  1495 	end
  1496 	return t and t[i].dynamic, path
  1497 end
  1498 
  1499 
  1500 function loona:dumphtml(o)
  1501 	local outbuf = { }
  1502 	o = o or { }
  1503 	o.nologin = true
  1504 	o.out = function(self, s) table.insert(outbuf, s) end
  1505 	o.setheader = function(self, s) end
  1506 	o = self:new(o):execute()
  1507 	if not o:isdynamic() then
  1508 		local path = o.sectionname
  1509 		path = path == o.config.defname and "index" or path
  1510 		local srcname = o.config.htdocsdir .. "/" .. path .. o.htmlext
  1511 		local fh, msg = open(srcname .. ".tmp", "wb")
  1512 		assert(fh, self:dbmsg("Could not write cached HTML", msg))
  1513 		fh:write(unpack(outbuf))
  1514 		fh:close()
  1515 		local dstname = o.config.htmlcachedir .. "/" .. path .. o.htmlext
  1516 		local success, msg = posix.symlink(srcname, dstname .. ".tmp")
  1517 -- 		assert(success, self:dbmsg("Could not link to cached HTML", msg))
  1518 	end
  1519 end