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